Automatic sources dropoff on 2020-06-10 18:32:38.095721

The change is generated with prebuilt drop tool.

Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/com/android/server/audio/AudioDeviceBroker.java b/com/android/server/audio/AudioDeviceBroker.java
new file mode 100644
index 0000000..40b6f42
--- /dev/null
+++ b/com/android/server/audio/AudioDeviceBroker.java
@@ -0,0 +1,1156 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothProfile;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioRoutesInfo;
+import android.media.AudioSystem;
+import android.media.IAudioRoutesObserver;
+import android.media.IStrategyPreferredDeviceDispatcher;
+import android.media.MediaMetrics;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.PrintWriterPrinter;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+
+/** @hide */
+/*package*/ final class AudioDeviceBroker {
+
+    private static final String TAG = "AS.AudioDeviceBroker";
+
+    private static final long BROKER_WAKELOCK_TIMEOUT_MS = 5000; //5s
+
+    /*package*/ static final  int BTA2DP_DOCK_TIMEOUT_MS = 8000;
+    // Timeout for connection to bluetooth headset service
+    /*package*/ static final int BT_HEADSET_CNCT_TIMEOUT_MS = 3000;
+
+    private final @NonNull AudioService mAudioService;
+    private final @NonNull Context mContext;
+
+    /** Forced device usage for communications sent to AudioSystem */
+    private int mForcedUseForComm;
+    /**
+     * Externally reported force device usage state returned by getters: always consistent
+     * with requests by setters */
+    private int mForcedUseForCommExt;
+
+    // Manages all connected devices, only ever accessed on the message loop
+    private final AudioDeviceInventory mDeviceInventory;
+    // Manages notifications to BT service
+    private final BtHelper mBtHelper;
+    // Adapter for system_server-reserved operations
+    private final SystemServerAdapter mSystemServer;
+
+
+    //-------------------------------------------------------------------
+    // we use a different lock than mDeviceStateLock so as not to create
+    // lock contention between enqueueing a message and handling them
+    private static final Object sLastDeviceConnectionMsgTimeLock = new Object();
+    @GuardedBy("sLastDeviceConnectionMsgTimeLock")
+    private static long sLastDeviceConnectMsgTime = 0;
+
+    // General lock to be taken whenever the state of the audio devices is to be checked or changed
+    private final Object mDeviceStateLock = new Object();
+
+    // Request to override default use of A2DP for media.
+    @GuardedBy("mDeviceStateLock")
+    private boolean mBluetoothA2dpEnabled;
+
+    // lock always taken when accessing AudioService.mSetModeDeathHandlers
+    // TODO do not "share" the lock between AudioService and BtHelpr, see b/123769055
+    /*package*/ final Object mSetModeLock = new Object();
+
+    //-------------------------------------------------------------------
+    /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) {
+        mContext = context;
+        mAudioService = service;
+        mBtHelper = new BtHelper(this);
+        mDeviceInventory = new AudioDeviceInventory(this);
+        mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);
+
+        init();
+    }
+
+    /** for test purposes only, inject AudioDeviceInventory and adapter for operations running
+     *  in system_server */
+    AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
+                      @NonNull AudioDeviceInventory mockDeviceInventory,
+                      @NonNull SystemServerAdapter mockSystemServer) {
+        mContext = context;
+        mAudioService = service;
+        mBtHelper = new BtHelper(this);
+        mDeviceInventory = mockDeviceInventory;
+        mSystemServer = mockSystemServer;
+
+        init();
+    }
+
+    private void init() {
+        setupMessaging(mContext);
+
+        mForcedUseForComm = AudioSystem.FORCE_NONE;
+        mForcedUseForCommExt = mForcedUseForComm;
+    }
+
+    /*package*/ Context getContext() {
+        return mContext;
+    }
+
+    //---------------------------------------------------------------------
+    // Communication from AudioService
+    // All methods are asynchronous and never block
+    // All permission checks are done in AudioService, all incoming calls are considered "safe"
+    // All post* methods are asynchronous
+
+    /*package*/ void onSystemReady() {
+        synchronized (mSetModeLock) {
+            synchronized (mDeviceStateLock) {
+                mBtHelper.onSystemReady();
+            }
+        }
+    }
+
+    /*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");
+        }
+        // restore devices
+        sendMsgNoDelay(MSG_RESTORE_DEVICES, SENDMSG_REPLACE);
+    }
+
+    /*package*/ void setForceUse_Async(int useCase, int config, String eventSource) {
+        sendIILMsgNoDelay(MSG_IIL_SET_FORCE_USE, SENDMSG_QUEUE,
+                useCase, config, eventSource);
+    }
+
+    /*package*/ void toggleHdmiIfConnected_Async() {
+        sendMsgNoDelay(MSG_TOGGLE_HDMI, SENDMSG_QUEUE);
+    }
+
+    /*package*/ void disconnectAllBluetoothProfiles() {
+        synchronized (mDeviceStateLock) {
+            mBtHelper.disconnectAllBluetoothProfiles();
+        }
+    }
+
+    /**
+     * Handle BluetoothHeadset intents where the action is one of
+     *   {@link BluetoothHeadset#ACTION_ACTIVE_DEVICE_CHANGED} or
+     *   {@link BluetoothHeadset#ACTION_AUDIO_STATE_CHANGED}.
+     * @param intent
+     */
+    /*package*/ void receiveBtEvent(@NonNull Intent intent) {
+        synchronized (mSetModeLock) {
+            synchronized (mDeviceStateLock) {
+                mBtHelper.receiveBtEvent(intent);
+            }
+        }
+    }
+
+    /*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) {
+        synchronized (mDeviceStateLock) {
+            if (mBluetoothA2dpEnabled == on) {
+                return;
+            }
+            mBluetoothA2dpEnabled = on;
+            mBrokerHandler.removeMessages(MSG_IIL_SET_FORCE_BT_A2DP_USE);
+            sendIILMsgNoDelay(MSG_IIL_SET_FORCE_BT_A2DP_USE, SENDMSG_QUEUE,
+                    AudioSystem.FOR_MEDIA,
+                    mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
+                    source);
+        }
+    }
+
+    /**
+     * Turns speakerphone on/off
+     * @param on
+     * @param eventSource for logging purposes
+     * @return true if speakerphone state changed
+     */
+    /*package*/ boolean setSpeakerphoneOn(boolean on, String eventSource) {
+        synchronized (mDeviceStateLock) {
+            final boolean wasOn = isSpeakerphoneOn();
+            if (on) {
+                if (mForcedUseForComm == AudioSystem.FORCE_BT_SCO) {
+                    setForceUse_Async(AudioSystem.FOR_RECORD, AudioSystem.FORCE_NONE, eventSource);
+                }
+                mForcedUseForComm = AudioSystem.FORCE_SPEAKER;
+            } else if (mForcedUseForComm == AudioSystem.FORCE_SPEAKER) {
+                mForcedUseForComm = AudioSystem.FORCE_NONE;
+            }
+
+            mForcedUseForCommExt = mForcedUseForComm;
+            setForceUse_Async(AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, eventSource);
+            return (wasOn != isSpeakerphoneOn());
+        }
+    }
+
+    /*package*/ boolean isSpeakerphoneOn() {
+        synchronized (mDeviceStateLock) {
+            return (mForcedUseForCommExt == AudioSystem.FORCE_SPEAKER);
+        }
+    }
+
+    /*package*/ void setWiredDeviceConnectionState(int type,
+            @AudioService.ConnectionState int state, String address, String name,
+            String caller) {
+        //TODO move logging here just like in setBluetooth* methods
+        synchronized (mDeviceStateLock) {
+            mDeviceInventory.setWiredDeviceConnectionState(type, state, address, name, caller);
+        }
+    }
+
+    private static final class BtDeviceConnectionInfo {
+        final @NonNull BluetoothDevice mDevice;
+        final @AudioService.BtProfileConnectionState int mState;
+        final int mProfile;
+        final boolean mSupprNoisy;
+        final int mVolume;
+
+        BtDeviceConnectionInfo(@NonNull BluetoothDevice device,
+                @AudioService.BtProfileConnectionState int state,
+                int profile, boolean suppressNoisyIntent, int vol) {
+            mDevice = device;
+            mState = state;
+            mProfile = profile;
+            mSupprNoisy = suppressNoisyIntent;
+            mVolume = vol;
+        }
+
+        // redefine equality op so we can match messages intended for this device
+        @Override
+        public boolean equals(Object o) {
+            if (o == null) {
+                return false;
+            }
+            if (this == o) {
+                return true;
+            }
+            if (o instanceof BtDeviceConnectionInfo) {
+                return mDevice.equals(((BtDeviceConnectionInfo) o).mDevice);
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "BtDeviceConnectionInfo dev=" + mDevice.toString();
+        }
+    }
+
+
+    /*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);
+
+        // operations of removing and posting messages related to A2DP device state change must be
+        // mutually exclusive
+        synchronized (mDeviceStateLock) {
+            // 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 that are already in
+            // the handler
+            removeScheduledA2dpEvents(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 state change events for the given device */
+    @GuardedBy("mDeviceStateLock")
+    private void removeScheduledA2dpEvents(@NonNull BluetoothDevice device) {
+        mBrokerHandler.removeEqualMessages(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, device);
+
+        final BtDeviceConnectionInfo connectionInfoToRemove = new BtDeviceConnectionInfo(device,
+                // the next parameters of the constructor will be ignored when finding the message
+                // to remove as the equality of the message's object is tested on the device itself
+                // (see BtDeviceConnectionInfo.equals() method override)
+                BluetoothProfile.STATE_CONNECTED, 0, false, -1);
+        mBrokerHandler.removeEqualMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
+                connectionInfoToRemove);
+        mBrokerHandler.removeEqualMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION,
+                connectionInfoToRemove);
+
+        final BtHelper.BluetoothA2dpDeviceInfo devInfoToRemove =
+                new BtHelper.BluetoothA2dpDeviceInfo(device);
+        mBrokerHandler.removeEqualMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
+                devInfoToRemove);
+        mBrokerHandler.removeEqualMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+                devInfoToRemove);
+        mBrokerHandler.removeEqualMessages(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE,
+                devInfoToRemove);
+    }
+
+    private static final class HearingAidDeviceConnectionInfo {
+        final @NonNull BluetoothDevice mDevice;
+        final @AudioService.BtProfileConnectionState int mState;
+        final boolean mSupprNoisy;
+        final int mMusicDevice;
+        final @NonNull String mEventSource;
+
+        HearingAidDeviceConnectionInfo(@NonNull BluetoothDevice device,
+                @AudioService.BtProfileConnectionState int state,
+                boolean suppressNoisyIntent, int musicDevice, @NonNull String eventSource) {
+            mDevice = device;
+            mState = state;
+            mSupprNoisy = suppressNoisyIntent;
+            mMusicDevice = musicDevice;
+            mEventSource = eventSource;
+        }
+    }
+
+    /*package*/ void postBluetoothHearingAidDeviceConnectionState(
+            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
+            boolean suppressNoisyIntent, int musicDevice, @NonNull String eventSource) {
+        final HearingAidDeviceConnectionInfo info = new HearingAidDeviceConnectionInfo(
+                device, state, suppressNoisyIntent, musicDevice, eventSource);
+        sendLMsgNoDelay(MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
+    }
+
+    // never called by system components
+    /*package*/ void setBluetoothScoOnByApp(boolean on) {
+        synchronized (mDeviceStateLock) {
+            mForcedUseForCommExt = on ? AudioSystem.FORCE_BT_SCO : AudioSystem.FORCE_NONE;
+        }
+    }
+
+    /*package*/ boolean isBluetoothScoOnForApp() {
+        synchronized (mDeviceStateLock) {
+            return mForcedUseForCommExt == AudioSystem.FORCE_BT_SCO;
+        }
+    }
+
+    /*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
+        //Log.i(TAG, "setBluetoothScoOn: " + on + " " + eventSource);
+        synchronized (mDeviceStateLock) {
+            if (on) {
+                // do not accept SCO ON if SCO audio is not connected
+                if (!mBtHelper.isBluetoothScoOn()) {
+                    mForcedUseForCommExt = AudioSystem.FORCE_BT_SCO;
+                    return;
+                }
+                mForcedUseForComm = AudioSystem.FORCE_BT_SCO;
+            } else if (mForcedUseForComm == AudioSystem.FORCE_BT_SCO) {
+                mForcedUseForComm = AudioSystem.FORCE_NONE;
+            }
+            mForcedUseForCommExt = mForcedUseForComm;
+            AudioSystem.setParameters("BT_SCO=" + (on ? "on" : "off"));
+            sendIILMsgNoDelay(MSG_IIL_SET_FORCE_USE, SENDMSG_QUEUE,
+                    AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, eventSource);
+            sendIILMsgNoDelay(MSG_IIL_SET_FORCE_USE, SENDMSG_QUEUE,
+                    AudioSystem.FOR_RECORD, mForcedUseForComm, eventSource);
+        }
+        // Un-mute ringtone stream volume
+        mAudioService.postUpdateRingerModeServiceInt();
+    }
+
+    /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
+        synchronized (mDeviceStateLock) {
+            return mDeviceInventory.startWatchingRoutes(observer);
+        }
+    }
+
+    /*package*/ AudioRoutesInfo getCurAudioRoutes() {
+        synchronized (mDeviceStateLock) {
+            return mDeviceInventory.getCurAudioRoutes();
+        }
+    }
+
+    /*package*/ boolean isAvrcpAbsoluteVolumeSupported() {
+        synchronized (mDeviceStateLock) {
+            return mBtHelper.isAvrcpAbsoluteVolumeSupported();
+        }
+    }
+
+    /*package*/ boolean isBluetoothA2dpOn() {
+        synchronized (mDeviceStateLock) {
+            return mBluetoothA2dpEnabled;
+        }
+    }
+
+    /*package*/ void postSetAvrcpAbsoluteVolumeIndex(int index) {
+        sendIMsgNoDelay(MSG_I_SET_AVRCP_ABSOLUTE_VOLUME, SENDMSG_REPLACE, index);
+    }
+
+    /*package*/ void postSetHearingAidVolumeIndex(int index, int streamType) {
+        sendIIMsgNoDelay(MSG_II_SET_HEARING_AID_VOLUME, SENDMSG_REPLACE, index, streamType);
+    }
+
+    /*package*/ void postDisconnectBluetoothSco(int exceptPid) {
+        sendIMsgNoDelay(MSG_I_DISCONNECT_BT_SCO, SENDMSG_REPLACE, exceptPid);
+    }
+
+    /*package*/ void postBluetoothA2dpDeviceConfigChange(@NonNull BluetoothDevice device) {
+        sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, device);
+    }
+
+    @GuardedBy("mSetModeLock")
+    /*package*/ void startBluetoothScoForClient_Sync(IBinder cb, int scoAudioMode,
+                @NonNull String eventSource) {
+        synchronized (mDeviceStateLock) {
+            mBtHelper.startBluetoothScoForClient(cb, scoAudioMode, eventSource);
+        }
+    }
+
+    @GuardedBy("mSetModeLock")
+    /*package*/ void stopBluetoothScoForClient_Sync(IBinder cb, @NonNull String eventSource) {
+        synchronized (mDeviceStateLock) {
+            mBtHelper.stopBluetoothScoForClient(cb, eventSource);
+        }
+    }
+
+    /*package*/ int setPreferredDeviceForStrategySync(int strategy,
+                                                      @NonNull AudioDeviceAttributes device) {
+        return mDeviceInventory.setPreferredDeviceForStrategySync(strategy, device);
+    }
+
+    /*package*/ int removePreferredDeviceForStrategySync(int strategy) {
+        return mDeviceInventory.removePreferredDeviceForStrategySync(strategy);
+    }
+
+    /*package*/ void registerStrategyPreferredDeviceDispatcher(
+            @NonNull IStrategyPreferredDeviceDispatcher dispatcher) {
+        mDeviceInventory.registerStrategyPreferredDeviceDispatcher(dispatcher);
+    }
+
+    /*package*/ void unregisterStrategyPreferredDeviceDispatcher(
+            @NonNull IStrategyPreferredDeviceDispatcher dispatcher) {
+        mDeviceInventory.unregisterStrategyPreferredDeviceDispatcher(dispatcher);
+    }
+
+    //---------------------------------------------------------------------
+    // Communication with (to) AudioService
+    //TODO check whether the AudioService methods are candidates to move here
+    /*package*/ void postAccessoryPlugMediaUnmute(int device) {
+        mAudioService.postAccessoryPlugMediaUnmute(device);
+    }
+
+    /*package*/ int getVssVolumeForDevice(int streamType, int device) {
+        return mAudioService.getVssVolumeForDevice(streamType, device);
+    }
+
+    /*package*/ int getModeOwnerPid() {
+        return mAudioService.getModeOwnerPid();
+    }
+
+    /*package*/ int getDeviceForStream(int streamType) {
+        return mAudioService.getDeviceForStream(streamType);
+    }
+
+    /*package*/ void postApplyVolumeOnDevice(int streamType, int device, String caller) {
+        mAudioService.postApplyVolumeOnDevice(streamType, device, caller);
+    }
+
+    /*package*/ void postSetVolumeIndexOnDevice(int streamType, int vssVolIndex, int device,
+                                                String caller) {
+        mAudioService.postSetVolumeIndexOnDevice(streamType, vssVolIndex, device, caller);
+    }
+
+    /*packages*/ void postObserveDevicesForAllStreams() {
+        mAudioService.postObserveDevicesForAllStreams();
+    }
+
+    /*package*/ boolean isInCommunication() {
+        return mAudioService.isInCommunication();
+    }
+
+    /*package*/ boolean hasMediaDynamicPolicy() {
+        return mAudioService.hasMediaDynamicPolicy();
+    }
+
+    /*package*/ ContentResolver getContentResolver() {
+        return mAudioService.getContentResolver();
+    }
+
+    /*package*/ void checkMusicActive(int deviceType, String caller) {
+        mAudioService.checkMusicActive(deviceType, caller);
+    }
+
+    /*package*/ void checkVolumeCecOnHdmiConnection(
+            @AudioService.ConnectionState  int state, String caller) {
+        mAudioService.postCheckVolumeCecOnHdmiConnection(state, caller);
+    }
+
+    /*package*/ boolean hasAudioFocusUsers() {
+        return mAudioService.hasAudioFocusUsers();
+    }
+
+    //---------------------------------------------------------------------
+    // Message handling on behalf of helper classes
+    /*package*/ void postBroadcastScoConnectionState(int state) {
+        sendIMsgNoDelay(MSG_I_BROADCAST_BT_CONNECTION_STATE, SENDMSG_QUEUE, state);
+    }
+
+    /*package*/ void postBroadcastBecomingNoisy() {
+        sendMsgNoDelay(MSG_BROADCAST_AUDIO_BECOMING_NOISY, SENDMSG_REPLACE);
+    }
+
+    @GuardedBy("mDeviceStateLock")
+    /*package*/ void postA2dpSinkConnection(@AudioService.BtProfileConnectionState int state,
+            @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
+        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(@AudioService.BtProfileConnectionState int state,
+            @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
+        sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE,
+                state, btDeviceInfo, delay);
+    }
+
+    /*package*/ void postSetWiredDeviceConnectionState(
+            AudioDeviceInventory.WiredDeviceConnectionState connectionState, int delay) {
+        sendLMsg(MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE, SENDMSG_QUEUE, connectionState, delay);
+    }
+
+    /*package*/ void postSetHearingAidConnectionState(
+            @AudioService.BtProfileConnectionState int state,
+            @NonNull BluetoothDevice device, int delay) {
+        sendILMsg(MSG_IL_SET_HEARING_AID_CONNECTION_STATE, SENDMSG_QUEUE,
+                state,
+                device,
+                delay);
+    }
+
+    /*package*/ void postDisconnectA2dp() {
+        sendMsgNoDelay(MSG_DISCONNECT_A2DP, SENDMSG_QUEUE);
+    }
+
+    /*package*/ void postDisconnectA2dpSink() {
+        sendMsgNoDelay(MSG_DISCONNECT_A2DP_SINK, SENDMSG_QUEUE);
+    }
+
+    /*package*/ void postDisconnectHearingAid() {
+        sendMsgNoDelay(MSG_DISCONNECT_BT_HEARING_AID, SENDMSG_QUEUE);
+    }
+
+    /*package*/ void postDisconnectHeadset() {
+        sendMsgNoDelay(MSG_DISCONNECT_BT_HEADSET, SENDMSG_QUEUE);
+    }
+
+    /*package*/ void postBtA2dpProfileConnected(BluetoothA2dp a2dpProfile) {
+        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP, SENDMSG_QUEUE, a2dpProfile);
+    }
+
+    /*package*/ void postBtA2dpSinkProfileConnected(BluetoothProfile profile) {
+        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK, SENDMSG_QUEUE, profile);
+    }
+
+    /*package*/ void postBtHeasetProfileConnected(BluetoothHeadset headsetProfile) {
+        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET, SENDMSG_QUEUE, headsetProfile);
+    }
+
+    /*package*/ void postBtHearingAidProfileConnected(BluetoothHearingAid hearingAidProfile) {
+        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID, SENDMSG_QUEUE,
+                hearingAidProfile);
+    }
+
+    /*package*/ void postScoClientDied(Object obj) {
+        sendLMsgNoDelay(MSG_L_SCOCLIENT_DIED, SENDMSG_QUEUE, obj);
+    }
+
+    /*package*/ void postSaveSetPreferredDeviceForStrategy(int strategy,
+                                                           AudioDeviceAttributes device)
+    {
+        sendILMsgNoDelay(MSG_IL_SAVE_PREF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
+    }
+
+    /*package*/ void postSaveRemovePreferredDeviceForStrategy(int strategy) {
+        sendIMsgNoDelay(MSG_I_SAVE_REMOVE_PREF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy);
+    }
+
+    //---------------------------------------------------------------------
+    // Method forwarding between the helper classes (BtHelper, AudioDeviceInventory)
+    // only call from a "handle"* method or "on"* method
+
+    // Handles request to override default use of A2DP for media.
+    //@GuardedBy("mConnectedDevices")
+    /*package*/ void setBluetoothA2dpOnInt(boolean on, String source) {
+        // for logging only
+        final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on)
+                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
+                .append(Binder.getCallingPid()).append(" src:").append(source).toString();
+
+        synchronized (mDeviceStateLock) {
+            mBluetoothA2dpEnabled = on;
+            mBrokerHandler.removeMessages(MSG_IIL_SET_FORCE_BT_A2DP_USE);
+            onSetForceUse(
+                    AudioSystem.FOR_MEDIA,
+                    mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
+                    eventSource);
+        }
+    }
+
+    /*package*/ boolean handleDeviceConnection(boolean connect, int device, String address,
+                                                       String deviceName) {
+        synchronized (mDeviceStateLock) {
+            return mDeviceInventory.handleDeviceConnection(connect, device, address, deviceName);
+        }
+    }
+
+    /*package*/ void postSetA2dpSourceConnectionState(@BluetoothProfile.BtProfileState int state,
+            @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
+        final int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
+        sendILMsgNoDelay(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE, state,
+                btDeviceInfo);
+    }
+
+    /*package*/ void handleFailureToConnectToBtHeadsetService(int delay) {
+        sendMsg(MSG_BT_HEADSET_CNCT_FAILED, SENDMSG_REPLACE, delay);
+    }
+
+    /*package*/ void handleCancelFailureToConnectToBtHeadsetService() {
+        mBrokerHandler.removeMessages(MSG_BT_HEADSET_CNCT_FAILED);
+    }
+
+    /*package*/ void postReportNewRoutes() {
+        sendMsgNoDelay(MSG_REPORT_NEW_ROUTES, SENDMSG_NOOP);
+    }
+
+    /*package*/ void postA2dpActiveDeviceChange(
+                    @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
+        sendLMsgNoDelay(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE, SENDMSG_QUEUE, btDeviceInfo);
+    }
+
+    // must be called synchronized on mConnectedDevices
+    /*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) {
+        final BtHelper.BluetoothA2dpDeviceInfo devInfoToCheck =
+                new BtHelper.BluetoothA2dpDeviceInfo(btDevice);
+        return (mBrokerHandler.hasEqualMessages(
+                    MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED, devInfoToCheck)
+                || mBrokerHandler.hasEqualMessages(
+                    MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED, devInfoToCheck));
+    }
+
+    /*package*/ void setA2dpTimeout(String address, int a2dpCodec, int delayMs) {
+        sendILMsg(MSG_IL_BTA2DP_TIMEOUT, SENDMSG_QUEUE, a2dpCodec, address, delayMs);
+    }
+
+    /*package*/ void setAvrcpAbsoluteVolumeSupported(boolean supported) {
+        synchronized (mDeviceStateLock) {
+            mBtHelper.setAvrcpAbsoluteVolumeSupported(supported);
+        }
+    }
+
+    /*package*/ boolean getBluetoothA2dpEnabled() {
+        synchronized (mDeviceStateLock) {
+            return mBluetoothA2dpEnabled;
+        }
+    }
+
+    /*package*/ int getA2dpCodec(@NonNull BluetoothDevice device) {
+        synchronized (mDeviceStateLock) {
+            return mBtHelper.getA2dpCodec(device);
+        }
+    }
+
+    /*package*/ void dump(PrintWriter pw, String prefix) {
+        if (mBrokerHandler != null) {
+            pw.println(prefix + "Message handler (watch for unhandled messages):");
+            mBrokerHandler.dump(new PrintWriterPrinter(pw), prefix + "  ");
+        } else {
+            pw.println("Message handler is null");
+        }
+        mDeviceInventory.dump(pw, prefix);
+    }
+
+    //---------------------------------------------------------------------
+    // Internal handling of messages
+    // These methods are ALL synchronous, in response to message handling in BrokerHandler
+    // Blocking in any of those will block the message queue
+
+    private void onSetForceUse(int useCase, int config, String eventSource) {
+        if (useCase == AudioSystem.FOR_MEDIA) {
+            postReportNewRoutes();
+        }
+        AudioService.sForceUseLogger.log(
+                new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource));
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_FORCE_USE + MediaMetrics.SEPARATOR
+                + AudioSystem.forceUseUsageToString(useCase))
+                .set(MediaMetrics.Property.EVENT, "onSetForceUse")
+                .set(MediaMetrics.Property.FORCE_USE_DUE_TO, eventSource)
+                .set(MediaMetrics.Property.FORCE_USE_MODE,
+                        AudioSystem.forceUseConfigToString(config))
+                .record();
+        AudioSystem.setForceUse(useCase, config);
+    }
+
+    private void onSendBecomingNoisyIntent() {
+        AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
+                "broadcast ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG));
+        mSystemServer.sendDeviceBecomingNoisyIntent();
+    }
+
+    //---------------------------------------------------------------------
+    // Message handling
+    private BrokerHandler mBrokerHandler;
+    private BrokerThread mBrokerThread;
+    private PowerManager.WakeLock mBrokerEventWakeLock;
+
+    private void setupMessaging(Context ctxt) {
+        final PowerManager pm = (PowerManager) ctxt.getSystemService(Context.POWER_SERVICE);
+        mBrokerEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                "handleAudioDeviceEvent");
+        mBrokerThread = new BrokerThread();
+        mBrokerThread.start();
+        waitForBrokerHandlerCreation();
+    }
+
+    private void waitForBrokerHandlerCreation() {
+        synchronized (this) {
+            while (mBrokerHandler == null) {
+                try {
+                    wait();
+                } catch (InterruptedException e) {
+                    Log.e(TAG, "Interruption while waiting on BrokerHandler");
+                }
+            }
+        }
+    }
+
+    /** Class that handles the device broker's message queue */
+    private class BrokerThread extends Thread {
+        BrokerThread() {
+            super("AudioDeviceBroker");
+        }
+
+        @Override
+        public void run() {
+            // Set this thread up so the handler will work on it
+            Looper.prepare();
+
+            synchronized (AudioDeviceBroker.this) {
+                mBrokerHandler = new BrokerHandler();
+
+                // Notify that the handler has been created
+                AudioDeviceBroker.this.notify();
+            }
+
+            Looper.loop();
+        }
+    }
+
+    /** Class that handles the message queue */
+    private class BrokerHandler extends Handler {
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_RESTORE_DEVICES:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.onRestoreDevices();
+                        mBtHelper.onAudioServerDiedRestoreA2dp();
+                    }
+                    break;
+                case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.onSetWiredDeviceConnectionState(
+                                (AudioDeviceInventory.WiredDeviceConnectionState) msg.obj);
+                    }
+                    break;
+                case MSG_I_BROADCAST_BT_CONNECTION_STATE:
+                    synchronized (mDeviceStateLock) {
+                        mBtHelper.onBroadcastScoConnectionState(msg.arg1);
+                    }
+                    break;
+                case MSG_IIL_SET_FORCE_USE: // intended fall-through
+                case MSG_IIL_SET_FORCE_BT_A2DP_USE:
+                    onSetForceUse(msg.arg1, msg.arg2, (String) msg.obj);
+                    break;
+                case MSG_REPORT_NEW_ROUTES:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.onReportNewRoutes();
+                    }
+                    break;
+                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);
+                    }
+                    break;
+                case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.onSetA2dpSourceConnectionState(
+                                (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
+                    }
+                    break;
+                case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.onSetHearingAidConnectionState(
+                                (BluetoothDevice) msg.obj, msg.arg1,
+                                mAudioService.getHearingAidStreamType());
+                    }
+                    break;
+                case MSG_BT_HEADSET_CNCT_FAILED:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mBtHelper.resetBluetoothSco();
+                        }
+                    }
+                    break;
+                case MSG_IL_BTA2DP_TIMEOUT:
+                    // msg.obj  == address of BTA2DP device
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.onMakeA2dpDeviceUnavailableNow((String) msg.obj, msg.arg1);
+                    }
+                    break;
+                case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
+                    final int a2dpCodec;
+                    final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
+                    synchronized (mDeviceStateLock) {
+                        a2dpCodec = mBtHelper.getA2dpCodec(btDevice);
+                        // TODO: name of method being called on AudioDeviceInventory is currently
+                        //       misleading (config change vs active device change), to be
+                        //       reconciliated once the BT side has been updated.
+                        mDeviceInventory.onBluetoothA2dpActiveDeviceChange(
+                                new BtHelper.BluetoothA2dpDeviceInfo(btDevice, -1, a2dpCodec),
+                                        BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
+                    }
+                    break;
+                case MSG_BROADCAST_AUDIO_BECOMING_NOISY:
+                    onSendBecomingNoisyIntent();
+                    break;
+                case MSG_II_SET_HEARING_AID_VOLUME:
+                    synchronized (mDeviceStateLock) {
+                        mBtHelper.setHearingAidVolume(msg.arg1, msg.arg2);
+                    }
+                    break;
+                case MSG_I_SET_AVRCP_ABSOLUTE_VOLUME:
+                    synchronized (mDeviceStateLock) {
+                        mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1);
+                    }
+                    break;
+                case MSG_I_DISCONNECT_BT_SCO:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mBtHelper.disconnectBluetoothSco(msg.arg1);
+                        }
+                    }
+                    break;
+                case MSG_L_SCOCLIENT_DIED:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mBtHelper.scoClientDied(msg.obj);
+                        }
+                    }
+                    break;
+                case MSG_TOGGLE_HDMI:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.onToggleHdmi();
+                    }
+                    break;
+                case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.onBluetoothA2dpActiveDeviceChange(
+                                (BtHelper.BluetoothA2dpDeviceInfo) msg.obj,
+                                 BtHelper.EVENT_ACTIVE_DEVICE_CHANGE);
+                    }
+                    break;
+                case MSG_DISCONNECT_A2DP:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.disconnectA2dp();
+                    }
+                    break;
+                case MSG_DISCONNECT_A2DP_SINK:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.disconnectA2dpSink();
+                    }
+                    break;
+                case MSG_DISCONNECT_BT_HEARING_AID:
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.disconnectHearingAid();
+                    }
+                    break;
+                case MSG_DISCONNECT_BT_HEADSET:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mBtHelper.disconnectHeadset();
+                        }
+                    }
+                    break;
+                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP:
+                    synchronized (mDeviceStateLock) {
+                        mBtHelper.onA2dpProfileConnected((BluetoothA2dp) msg.obj);
+                    }
+                    break;
+                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK:
+                    synchronized (mDeviceStateLock) {
+                        mBtHelper.onA2dpSinkProfileConnected((BluetoothProfile) msg.obj);
+                    }
+                    break;
+                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID:
+                    synchronized (mDeviceStateLock) {
+                        mBtHelper.onHearingAidProfileConnected((BluetoothHearingAid) msg.obj);
+                    }
+                    break;
+                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mBtHelper.onHeadsetProfileConnected((BluetoothHeadset) msg.obj);
+                        }
+                    }
+                    break;
+                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(
+                            "msg: setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent "
+                                    + " state=" + info.mState
+                                    // only querying address as this is the only readily available
+                                    // field on the device
+                                    + " addr=" + info.mDevice.getAddress()
+                                    + " prof=" + info.mProfile + " supprNoisy=" + info.mSupprNoisy
+                                    + " vol=" + info.mVolume)).printLog(TAG));
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.setBluetoothA2dpDeviceConnectionState(
+                                info.mDevice, info.mState, info.mProfile, info.mSupprNoisy,
+                                AudioSystem.DEVICE_NONE, info.mVolume);
+                    }
+                } break;
+                case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT: {
+                    final HearingAidDeviceConnectionInfo info =
+                            (HearingAidDeviceConnectionInfo) msg.obj;
+                    AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
+                            "msg: setHearingAidDeviceConnectionState state=" + info.mState
+                                    + " addr=" + info.mDevice.getAddress()
+                                    + " supprNoisy=" + info.mSupprNoisy
+                                    + " src=" + info.mEventSource)).printLog(TAG));
+                    synchronized (mDeviceStateLock) {
+                        mDeviceInventory.setBluetoothHearingAidDeviceConnectionState(
+                                info.mDevice, info.mState, info.mSupprNoisy, info.mMusicDevice);
+                    }
+                } break;
+                case MSG_IL_SAVE_PREF_DEVICE_FOR_STRATEGY: {
+                    final int strategy = msg.arg1;
+                    final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
+                    mDeviceInventory.onSaveSetPreferredDevice(strategy, device);
+                } break;
+                case MSG_I_SAVE_REMOVE_PREF_DEVICE_FOR_STRATEGY: {
+                    final int strategy = msg.arg1;
+                    mDeviceInventory.onSaveRemovePreferredDevice(strategy);
+                } break;
+                default:
+                    Log.wtf(TAG, "Invalid message " + msg.what);
+            }
+            if (isMessageHandledUnderWakelock(msg.what)) {
+                try {
+                    mBrokerEventWakeLock.release();
+                } catch (Exception e) {
+                    Log.e(TAG, "Exception releasing wakelock", e);
+                }
+            }
+        }
+    }
+
+    // List of all messages. If a message has be handled under wakelock, add it to
+    //    the isMessageHandledUnderWakelock(int) method
+    // Naming of msg indicates arguments, using JNI argument grammar
+    // (e.g. II indicates two int args, IL indicates int and Obj arg)
+    private static final int MSG_RESTORE_DEVICES = 1;
+    private static final int MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE = 2;
+    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_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;
+    private static final int MSG_IL_BTA2DP_TIMEOUT = 10;
+
+    // process change of A2DP device configuration, obj is BluetoothDevice
+    private static final int MSG_L_A2DP_DEVICE_CONFIG_CHANGE = 11;
+
+    private static final int MSG_BROADCAST_AUDIO_BECOMING_NOISY = 12;
+    private static final int MSG_REPORT_NEW_ROUTES = 13;
+    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;
+
+    // process active A2DP device change, obj is BtHelper.BluetoothA2dpDeviceInfo
+    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;
+    private static final int MSG_DISCONNECT_BT_HEARING_AID = 21;
+    private static final int MSG_DISCONNECT_BT_HEADSET = 22;
+    private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP = 23;
+    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;
+
+    // process change of state, obj is BtHelper.BluetoothA2dpDeviceInfo
+    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, obj is BtDeviceConnectionInfo
+    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 = 31;
+
+    // a ScoClient died in BtHelper
+    private static final int MSG_L_SCOCLIENT_DIED = 32;
+    private static final int MSG_IL_SAVE_PREF_DEVICE_FOR_STRATEGY = 33;
+    private static final int MSG_I_SAVE_REMOVE_PREF_DEVICE_FOR_STRATEGY = 34;
+
+
+    private static boolean isMessageHandledUnderWakelock(int msgId) {
+        switch(msgId) {
+            case MSG_L_SET_WIRED_DEVICE_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_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_CONNECTION:
+            case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION:
+            case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    // Message helper methods
+
+    // sendMsg() flags
+    /** If the msg is already queued, replace it with this one. */
+    private static final int SENDMSG_REPLACE = 0;
+    /** If the msg is already queued, ignore this one and leave the old. */
+    private static final int SENDMSG_NOOP = 1;
+    /** If the msg is already queued, queue this one and leave the old. */
+    private static final int SENDMSG_QUEUE = 2;
+
+    private void sendMsg(int msg, int existingMsgPolicy, int delay) {
+        sendIILMsg(msg, existingMsgPolicy, 0, 0, null, delay);
+    }
+
+    private void sendILMsg(int msg, int existingMsgPolicy, int arg, Object obj, int delay) {
+        sendIILMsg(msg, existingMsgPolicy, arg, 0, obj, delay);
+    }
+
+    private void sendLMsg(int msg, int existingMsgPolicy, Object obj, int delay) {
+        sendIILMsg(msg, existingMsgPolicy, 0, 0, obj, delay);
+    }
+
+    private void sendIMsg(int msg, int existingMsgPolicy, int arg, int delay) {
+        sendIILMsg(msg, existingMsgPolicy, arg, 0, null, delay);
+    }
+
+    private void sendMsgNoDelay(int msg, int existingMsgPolicy) {
+        sendIILMsg(msg, existingMsgPolicy, 0, 0, null, 0);
+    }
+
+    private void sendIMsgNoDelay(int msg, int existingMsgPolicy, int arg) {
+        sendIILMsg(msg, existingMsgPolicy, arg, 0, null, 0);
+    }
+
+    private void sendIIMsgNoDelay(int msg, int existingMsgPolicy, int arg1, int arg2) {
+        sendIILMsg(msg, existingMsgPolicy, arg1, arg2, null, 0);
+    }
+
+    private void sendILMsgNoDelay(int msg, int existingMsgPolicy, int arg, Object obj) {
+        sendIILMsg(msg, existingMsgPolicy, arg, 0, obj, 0);
+    }
+
+    private void sendLMsgNoDelay(int msg, int existingMsgPolicy, Object obj) {
+        sendIILMsg(msg, existingMsgPolicy, 0, 0, obj, 0);
+    }
+
+    private void sendIILMsgNoDelay(int msg, int existingMsgPolicy, int arg1, int arg2, Object obj) {
+        sendIILMsg(msg, existingMsgPolicy, arg1, arg2, obj, 0);
+    }
+
+    private void sendIILMsg(int msg, int existingMsgPolicy, int arg1, int arg2, Object obj,
+                            int delay) {
+        if (existingMsgPolicy == SENDMSG_REPLACE) {
+            mBrokerHandler.removeMessages(msg);
+        } else if (existingMsgPolicy == SENDMSG_NOOP && mBrokerHandler.hasMessages(msg)) {
+            return;
+        }
+
+        if (isMessageHandledUnderWakelock(msg)) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mBrokerEventWakeLock.acquire(BROKER_WAKELOCK_TIMEOUT_MS);
+            } catch (Exception e) {
+                Log.e(TAG, "Exception acquiring wakelock", e);
+            }
+            Binder.restoreCallingIdentity(identity);
+        }
+
+        synchronized (sLastDeviceConnectionMsgTimeLock) {
+            long time = SystemClock.uptimeMillis() + delay;
+
+            switch (msg) {
+                case MSG_IL_SET_A2DP_SOURCE_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_TIMEOUT:
+                case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
+                case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
+                    if (sLastDeviceConnectMsgTime >= time) {
+                        // add a little delay to make sure messages are ordered as expected
+                        time = sLastDeviceConnectMsgTime + 30;
+                    }
+                    sLastDeviceConnectMsgTime = time;
+                    break;
+                default:
+                    break;
+            }
+
+            mBrokerHandler.sendMessageAtTime(mBrokerHandler.obtainMessage(msg, arg1, arg2, obj),
+                    time);
+        }
+    }
+}
diff --git a/com/android/server/audio/AudioDeviceInventory.java b/com/android/server/audio/AudioDeviceInventory.java
new file mode 100644
index 0000000..b1f8fee
--- /dev/null
+++ b/com/android/server/audio/AudioDeviceInventory.java
@@ -0,0 +1,1318 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothProfile;
+import android.content.Intent;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDevicePort;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioPort;
+import android.media.AudioRoutesInfo;
+import android.media.AudioSystem;
+import android.media.IAudioRoutesObserver;
+import android.media.IStrategyPreferredDeviceDispatcher;
+import android.media.MediaMetrics;
+import android.os.Binder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Set;
+
+/**
+ * Class to manage the inventory of all connected devices.
+ * This class is thread-safe.
+ * (non final for mocking/spying)
+ */
+public class AudioDeviceInventory {
+
+    private static final String TAG = "AS.AudioDeviceInventory";
+
+    // lock to synchronize all access to mConnectedDevices and mApmConnectedDevices
+    private final Object mDevicesLock = new Object();
+
+    //Audio Analytics ids.
+    private static final String mMetricsId = "audio.device.";
+
+    // List of connected devices
+    // Key for map created from DeviceInfo.makeDeviceListKey()
+    @GuardedBy("mDevicesLock")
+    private final LinkedHashMap<String, DeviceInfo> mConnectedDevices = new LinkedHashMap<>() {
+        @Override
+        public DeviceInfo put(String key, DeviceInfo value) {
+            final DeviceInfo result = super.put(key, value);
+            record("put", true /* connected */, key, value);
+            return result;
+        }
+
+        @Override
+        public DeviceInfo putIfAbsent(String key, DeviceInfo value) {
+            final DeviceInfo result = super.putIfAbsent(key, value);
+            if (result == null) {
+                record("putIfAbsent", true /* connected */, key, value);
+            }
+            return result;
+        }
+
+        @Override
+        public DeviceInfo remove(Object key) {
+            final DeviceInfo result = super.remove(key);
+            if (result != null) {
+                record("remove", false /* connected */, (String) key, result);
+            }
+            return result;
+        }
+
+        @Override
+        public boolean remove(Object key, Object value) {
+            final boolean result = super.remove(key, value);
+            if (result) {
+                record("remove", false /* connected */, (String) key, (DeviceInfo) value);
+            }
+            return result;
+        }
+
+        // Not overridden
+        // clear
+        // compute
+        // computeIfAbsent
+        // computeIfPresent
+        // merge
+        // putAll
+        // replace
+        // replaceAll
+        private void record(String event, boolean connected, String key, DeviceInfo value) {
+            // DeviceInfo - int mDeviceType;
+            // DeviceInfo - int mDeviceCodecFormat;
+            new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+                    + MediaMetrics.SEPARATOR + AudioSystem.getDeviceName(value.mDeviceType))
+                    .set(MediaMetrics.Property.ADDRESS, value.mDeviceAddress)
+                    .set(MediaMetrics.Property.EVENT, event)
+                    .set(MediaMetrics.Property.NAME, value.mDeviceName)
+                    .set(MediaMetrics.Property.STATE, connected
+                            ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
+                    .record();
+        }
+    };
+
+    // List of devices actually connected to AudioPolicy (through AudioSystem), only one
+    // by device type, which is used as the key, value is the DeviceInfo generated key.
+    // For the moment only for A2DP sink devices.
+    // TODO: extend to all device types
+    @GuardedBy("mDevicesLock")
+    private final ArrayMap<Integer, String> mApmConnectedDevices = new ArrayMap<>();
+
+    // List of preferred devices for strategies
+    private final ArrayMap<Integer, AudioDeviceAttributes> mPreferredDevices = new ArrayMap<>();
+
+    // the wrapper for AudioSystem static methods, allows us to spy AudioSystem
+    private final @NonNull AudioSystemAdapter mAudioSystem;
+
+    private @NonNull AudioDeviceBroker mDeviceBroker;
+
+    // Monitoring of audio routes.  Protected by mAudioRoutes.
+    final AudioRoutesInfo mCurAudioRoutes = new AudioRoutesInfo();
+    final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers =
+            new RemoteCallbackList<IAudioRoutesObserver>();
+
+    // Monitoring of strategy-preferred device
+    final RemoteCallbackList<IStrategyPreferredDeviceDispatcher> mPrefDevDispatchers =
+            new RemoteCallbackList<IStrategyPreferredDeviceDispatcher>();
+
+    /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
+        mDeviceBroker = broker;
+        mAudioSystem = AudioSystemAdapter.getDefaultAdapter();
+    }
+
+    //-----------------------------------------------------------
+    /** for mocking only, allows to inject AudioSystem adapter */
+    /*package*/ AudioDeviceInventory(@NonNull AudioSystemAdapter audioSystem) {
+        mDeviceBroker = null;
+        mAudioSystem = audioSystem;
+    }
+
+    /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) {
+        mDeviceBroker = broker;
+    }
+
+    //------------------------------------------------------------
+    /**
+     * Class to store info about connected devices.
+     * Use makeDeviceListKey() to make a unique key for this list.
+     */
+    private static class DeviceInfo {
+        final int mDeviceType;
+        final @NonNull String mDeviceName;
+        final @NonNull String mDeviceAddress;
+        int mDeviceCodecFormat;
+
+        DeviceInfo(int deviceType, String deviceName, String deviceAddress, int deviceCodecFormat) {
+            mDeviceType = deviceType;
+            mDeviceName = deviceName == null ? "" : deviceName;
+            mDeviceAddress = deviceAddress == null ? "" : deviceAddress;
+            mDeviceCodecFormat = deviceCodecFormat;
+        }
+
+        @Override
+        public String toString() {
+            return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType)
+                    + " (" + AudioSystem.getDeviceName(mDeviceType)
+                    + ") name:" + mDeviceName
+                    + " addr:" + mDeviceAddress
+                    + " codec: " + Integer.toHexString(mDeviceCodecFormat) + "]";
+        }
+
+        @NonNull String getKey() {
+            return makeDeviceListKey(mDeviceType, mDeviceAddress);
+        }
+
+        /**
+         * Generate a unique key for the mConnectedDevices List by composing the device "type"
+         * and the "address" associated with a specific instance of that device type
+         */
+        @NonNull private static String makeDeviceListKey(int device, String deviceAddress) {
+            return "0x" + Integer.toHexString(device) + ":" + deviceAddress;
+        }
+    }
+
+    /**
+     * A class just for packaging up a set of connection parameters.
+     */
+    /*package*/ class WiredDeviceConnectionState {
+        public final int mType;
+        public final @AudioService.ConnectionState int mState;
+        public final String mAddress;
+        public final String mName;
+        public final String mCaller;
+
+        /*package*/ WiredDeviceConnectionState(int type, @AudioService.ConnectionState int state,
+                                               String address, String name, String caller) {
+            mType = type;
+            mState = state;
+            mAddress = address;
+            mName = name;
+            mCaller = caller;
+        }
+    }
+
+    //------------------------------------------------------------
+    /*package*/ void dump(PrintWriter pw, String prefix) {
+        pw.println("\n" + prefix + "Preferred devices for strategy:");
+        mPreferredDevices.forEach((strategy, device) -> {
+            pw.println("  " + prefix + "strategy:" + strategy + " device:" + device); });
+        pw.println("\n" + prefix + "Connected devices:");
+        mConnectedDevices.forEach((key, deviceInfo) -> {
+            pw.println("  " + prefix + deviceInfo.toString()); });
+        pw.println("\n" + prefix + "APM Connected device (A2DP sink only):");
+        mApmConnectedDevices.forEach((keyType, valueAddress) -> {
+            pw.println("  " + prefix + " type:0x" + Integer.toHexString(keyType)
+                    + " (" + AudioSystem.getDeviceName(keyType)
+                    + ") addr:" + valueAddress); });
+    }
+
+    //------------------------------------------------------------
+    // Message handling from AudioDeviceBroker
+
+    /**
+     * Restore previously connected devices. Use in case of audio server crash
+     * (see AudioService.onAudioServerDied() method)
+     */
+    // Always executed on AudioDeviceBroker message queue
+    /*package*/ void onRestoreDevices() {
+        synchronized (mDevicesLock) {
+            //TODO iterate on mApmConnectedDevices instead once it handles all device types
+            for (DeviceInfo di : mConnectedDevices.values()) {
+                mAudioSystem.setDeviceConnectionState(
+                        di.mDeviceType,
+                        AudioSystem.DEVICE_STATE_AVAILABLE,
+                        di.mDeviceAddress,
+                        di.mDeviceName,
+                        di.mDeviceCodecFormat);
+            }
+        }
+        synchronized (mPreferredDevices) {
+            mPreferredDevices.forEach((strategy, device) -> {
+                mAudioSystem.setPreferredDeviceForStrategy(strategy, device); });
+        }
+    }
+
+    // only public for mocking/spying
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    @VisibleForTesting
+    public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
+            @AudioService.BtProfileConnectionState int state) {
+        final BluetoothDevice btDevice = btInfo.getBtDevice();
+        int a2dpVolume = btInfo.getVolume();
+        if (AudioService.DEBUG_DEVICES) {
+            Log.d(TAG, "onSetA2dpSinkConnectionState btDevice=" + btDevice + " state="
+                    + state + " vol=" + a2dpVolume);
+        }
+        String address = btDevice.getAddress();
+        if (address == null) {
+            address = "";
+        }
+        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+            address = "";
+        }
+
+        final @AudioSystem.AudioFormatNativeEnumForBtCodec int a2dpCodec = btInfo.getCodec();
+
+        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                "A2DP sink connected: device addr=" + address + " state=" + state
+                        + " codec=" + AudioSystem.audioFormatToString(a2dpCodec)
+                        + " vol=" + a2dpVolume));
+
+        new MediaMetrics.Item(mMetricsId + "a2dp")
+                .set(MediaMetrics.Property.ADDRESS, address)
+                .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(a2dpCodec))
+                .set(MediaMetrics.Property.EVENT, "onSetA2dpSinkConnectionState")
+                .set(MediaMetrics.Property.INDEX, a2dpVolume)
+                .set(MediaMetrics.Property.STATE,
+                        state == BluetoothProfile.STATE_CONNECTED
+                        ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
+                .record();
+
+        synchronized (mDevicesLock) {
+            final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                    btDevice.getAddress());
+            final DeviceInfo di = mConnectedDevices.get(key);
+            boolean isConnected = di != null;
+
+            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);
+                }
+            } else if (state == BluetoothProfile.STATE_CONNECTED) {
+                // device is not already connected
+                if (a2dpVolume != -1) {
+                    mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
+                            // convert index to internal representation in VolumeStreamState
+                            a2dpVolume * 10,
+                            AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "onSetA2dpSinkConnectionState");
+                }
+                makeA2dpDeviceAvailable(address, BtHelper.getName(btDevice),
+                        "onSetA2dpSinkConnectionState", a2dpCodec);
+            }
+        }
+    }
+
+    /*package*/ void onSetA2dpSourceConnectionState(
+            @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int state) {
+        final BluetoothDevice btDevice = btInfo.getBtDevice();
+        if (AudioService.DEBUG_DEVICES) {
+            Log.d(TAG, "onSetA2dpSourceConnectionState btDevice=" + btDevice + " state="
+                    + state);
+        }
+        String address = btDevice.getAddress();
+        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+            address = "";
+        }
+
+        synchronized (mDevicesLock) {
+            final String key = DeviceInfo.makeDeviceListKey(
+                    AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address);
+            final DeviceInfo di = mConnectedDevices.get(key);
+            boolean isConnected = di != null;
+
+            new MediaMetrics.Item(mMetricsId + "onSetA2dpSourceConnectionState")
+                    .set(MediaMetrics.Property.ADDRESS, address)
+                    .set(MediaMetrics.Property.DEVICE,
+                            AudioSystem.getDeviceName(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP))
+                    .set(MediaMetrics.Property.STATE,
+                            state == BluetoothProfile.STATE_CONNECTED
+                            ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
+                    .record();
+
+            if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
+                makeA2dpSrcUnavailable(address);
+            } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
+                makeA2dpSrcAvailable(address);
+            }
+        }
+    }
+
+    /*package*/ void onSetHearingAidConnectionState(BluetoothDevice btDevice,
+                @AudioService.BtProfileConnectionState int state, int streamType) {
+        String address = btDevice.getAddress();
+        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+            address = "";
+        }
+        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                "onSetHearingAidConnectionState addr=" + address));
+
+        new MediaMetrics.Item(mMetricsId + "onSetHearingAidConnectionState")
+                .set(MediaMetrics.Property.ADDRESS, address)
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP))
+                .set(MediaMetrics.Property.STATE,
+                        state == BluetoothProfile.STATE_CONNECTED
+                                ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
+                .set(MediaMetrics.Property.STREAM_TYPE,
+                        AudioSystem.streamToString(streamType))
+                .record();
+
+        synchronized (mDevicesLock) {
+            final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID,
+                    btDevice.getAddress());
+            final DeviceInfo di = mConnectedDevices.get(key);
+            boolean isConnected = di != null;
+
+            if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
+                makeHearingAidDeviceUnavailable(address);
+            } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
+                makeHearingAidDeviceAvailable(address, BtHelper.getName(btDevice), streamType,
+                        "onSetHearingAidConnectionState");
+            }
+        }
+    }
+
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+        /*package*/ void onBluetoothA2dpActiveDeviceChange(
+            @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
+                + "onBluetoothA2dpActiveDeviceChange")
+                .set(MediaMetrics.Property.EVENT, BtHelper.a2dpDeviceEventToString(event));
+
+        final BluetoothDevice btDevice = btInfo.getBtDevice();
+        if (btDevice == null) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "btDevice null").record();
+            return;
+        }
+        if (AudioService.DEBUG_DEVICES) {
+            Log.d(TAG, "onBluetoothA2dpActiveDeviceChange btDevice=" + btDevice);
+        }
+        int a2dpVolume = btInfo.getVolume();
+        @AudioSystem.AudioFormatNativeEnumForBtCodec final int a2dpCodec = btInfo.getCodec();
+
+        String address = btDevice.getAddress();
+        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+            address = "";
+        }
+        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                "onBluetoothA2dpActiveDeviceChange addr=" + address
+                    + " event=" + BtHelper.a2dpDeviceEventToString(event)));
+
+        synchronized (mDevicesLock) {
+            if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) {
+                AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                        "A2dp config change ignored (scheduled connection change)")
+                        .printLog(TAG));
+                mmi.set(MediaMetrics.Property.EARLY_RETURN, "A2dp config change ignored")
+                        .record();
+                return;
+            }
+            final String key = DeviceInfo.makeDeviceListKey(
+                    AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
+            final DeviceInfo di = mConnectedDevices.get(key);
+            if (di == null) {
+                Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpActiveDeviceChange");
+                mmi.set(MediaMetrics.Property.EARLY_RETURN, "null DeviceInfo").record();
+                return;
+            }
+
+            mmi.set(MediaMetrics.Property.ADDRESS, address)
+                    .set(MediaMetrics.Property.ENCODING,
+                            AudioSystem.audioFormatToString(a2dpCodec))
+                    .set(MediaMetrics.Property.INDEX, a2dpVolume)
+                    .set(MediaMetrics.Property.NAME, di.mDeviceName);
+
+            if (event == BtHelper.EVENT_ACTIVE_DEVICE_CHANGE) {
+                // Device is connected
+                if (a2dpVolume != -1) {
+                    mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
+                            // convert index to internal representation in VolumeStreamState
+                            a2dpVolume * 10,
+                            AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                            "onBluetoothA2dpActiveDeviceChange");
+                }
+            } else if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) {
+                if (di.mDeviceCodecFormat != a2dpCodec) {
+                    di.mDeviceCodecFormat = a2dpCodec;
+                    mConnectedDevices.replace(key, di);
+                }
+            }
+            final int res = mAudioSystem.handleDeviceConfigChange(
+                    AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address,
+                    BtHelper.getName(btDevice), a2dpCodec);
+
+            if (res != AudioSystem.AUDIO_STATUS_OK) {
+                AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                        "APM handleDeviceConfigChange failed for A2DP device addr=" + address
+                                + " codec=" + AudioSystem.audioFormatToString(a2dpCodec))
+                        .printLog(TAG));
+
+                int musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC);
+                // force A2DP device disconnection in case of error so that AudioService state is
+                // consistent with audio policy manager state
+                setBluetoothA2dpDeviceConnectionState(
+                        btDevice, BluetoothA2dp.STATE_DISCONNECTED, BluetoothProfile.A2DP,
+                        false /* suppressNoisyIntent */, musicDevice,
+                        -1 /* a2dpVolume */);
+            } else {
+                AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                        "APM handleDeviceConfigChange success for A2DP device addr=" + address
+                                + " codec=" + AudioSystem.audioFormatToString(a2dpCodec))
+                        .printLog(TAG));
+            }
+        }
+        mmi.record();
+    }
+
+    /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
+        synchronized (mDevicesLock) {
+            makeA2dpDeviceUnavailableNow(address, a2dpCodec);
+        }
+    }
+
+    /*package*/ void onReportNewRoutes() {
+        int n = mRoutesObservers.beginBroadcast();
+        if (n > 0) {
+            new MediaMetrics.Item(mMetricsId + "onReportNewRoutes")
+                    .set(MediaMetrics.Property.OBSERVERS, n)
+                    .record();
+            AudioRoutesInfo routes;
+            synchronized (mCurAudioRoutes) {
+                routes = new AudioRoutesInfo(mCurAudioRoutes);
+            }
+            while (n > 0) {
+                n--;
+                IAudioRoutesObserver obs = mRoutesObservers.getBroadcastItem(n);
+                try {
+                    obs.dispatchAudioRoutesChanged(routes);
+                } catch (RemoteException e) { }
+            }
+        }
+        mRoutesObservers.finishBroadcast();
+        mDeviceBroker.postObserveDevicesForAllStreams();
+    }
+
+    private static final Set<Integer> DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET;
+    static {
+        DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET = new HashSet<>();
+        DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADSET);
+        DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE);
+        DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_LINE);
+        DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET);
+    }
+
+    /*package*/ void onSetWiredDeviceConnectionState(
+                            AudioDeviceInventory.WiredDeviceConnectionState wdcs) {
+        AudioService.sDeviceLogger.log(new AudioServiceEvents.WiredDevConnectEvent(wdcs));
+
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
+                + "onSetWiredDeviceConnectionState")
+                .set(MediaMetrics.Property.ADDRESS, wdcs.mAddress)
+                .set(MediaMetrics.Property.DEVICE, AudioSystem.getDeviceName(wdcs.mType))
+                .set(MediaMetrics.Property.STATE,
+                        wdcs.mState == AudioService.CONNECTION_STATE_DISCONNECTED
+                                ? MediaMetrics.Value.DISCONNECTED : MediaMetrics.Value.CONNECTED);
+        synchronized (mDevicesLock) {
+            if ((wdcs.mState == AudioService.CONNECTION_STATE_DISCONNECTED)
+                    && DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.contains(wdcs.mType)) {
+                mDeviceBroker.setBluetoothA2dpOnInt(true,
+                        "onSetWiredDeviceConnectionState state DISCONNECTED");
+            }
+
+            if (!handleDeviceConnection(wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED,
+                    wdcs.mType, wdcs.mAddress, wdcs.mName)) {
+                // change of connection state failed, bailout
+                mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed")
+                        .record();
+                return;
+            }
+            if (wdcs.mState != AudioService.CONNECTION_STATE_DISCONNECTED) {
+                if (DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.contains(wdcs.mType)) {
+                    mDeviceBroker.setBluetoothA2dpOnInt(false,
+                            "onSetWiredDeviceConnectionState state not DISCONNECTED");
+                }
+                mDeviceBroker.checkMusicActive(wdcs.mType, wdcs.mCaller);
+            }
+            if (wdcs.mType == AudioSystem.DEVICE_OUT_HDMI) {
+                mDeviceBroker.checkVolumeCecOnHdmiConnection(wdcs.mState, wdcs.mCaller);
+            }
+            sendDeviceConnectionIntent(wdcs.mType, wdcs.mState, wdcs.mAddress, wdcs.mName);
+            updateAudioRoutes(wdcs.mType, wdcs.mState);
+        }
+        mmi.record();
+    }
+
+    /*package*/ void onToggleHdmi() {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi")
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HDMI));
+        synchronized (mDevicesLock) {
+            // Is HDMI connected?
+            final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HDMI, "");
+            final DeviceInfo di = mConnectedDevices.get(key);
+            if (di == null) {
+                Log.e(TAG, "invalid null DeviceInfo in onToggleHdmi");
+                mmi.set(MediaMetrics.Property.EARLY_RETURN, "invalid null DeviceInfo").record();
+                return;
+            }
+            // Toggle HDMI to retrigger broadcast with proper formats.
+            setWiredDeviceConnectionState(AudioSystem.DEVICE_OUT_HDMI,
+                    AudioSystem.DEVICE_STATE_UNAVAILABLE, "", "",
+                    "android"); // disconnect
+            setWiredDeviceConnectionState(AudioSystem.DEVICE_OUT_HDMI,
+                    AudioSystem.DEVICE_STATE_AVAILABLE, "", "",
+                    "android"); // reconnect
+        }
+        mmi.record();
+    }
+
+    /*package*/ void onSaveSetPreferredDevice(int strategy, @NonNull AudioDeviceAttributes device) {
+        mPreferredDevices.put(strategy, device);
+        dispatchPreferredDevice(strategy, device);
+    }
+
+    /*package*/ void onSaveRemovePreferredDevice(int strategy) {
+        mPreferredDevices.remove(strategy);
+        dispatchPreferredDevice(strategy, null);
+    }
+
+    //------------------------------------------------------------
+    //
+
+    /*package*/ int setPreferredDeviceForStrategySync(int strategy,
+                                                      @NonNull AudioDeviceAttributes device) {
+        final long identity = Binder.clearCallingIdentity();
+        final int status = mAudioSystem.setPreferredDeviceForStrategy(strategy, device);
+        Binder.restoreCallingIdentity(identity);
+
+        if (status == AudioSystem.SUCCESS) {
+            mDeviceBroker.postSaveSetPreferredDeviceForStrategy(strategy, device);
+        }
+        return status;
+    }
+
+    /*package*/ int removePreferredDeviceForStrategySync(int strategy) {
+        final long identity = Binder.clearCallingIdentity();
+        final int status = mAudioSystem.removePreferredDeviceForStrategy(strategy);
+        Binder.restoreCallingIdentity(identity);
+
+        if (status == AudioSystem.SUCCESS) {
+            mDeviceBroker.postSaveRemovePreferredDeviceForStrategy(strategy);
+        }
+        return status;
+    }
+
+    /*package*/ void registerStrategyPreferredDeviceDispatcher(
+            @NonNull IStrategyPreferredDeviceDispatcher dispatcher) {
+        mPrefDevDispatchers.register(dispatcher);
+    }
+
+    /*package*/ void unregisterStrategyPreferredDeviceDispatcher(
+            @NonNull IStrategyPreferredDeviceDispatcher dispatcher) {
+        mPrefDevDispatchers.unregister(dispatcher);
+    }
+
+    /**
+     * Implements the communication with AudioSystem to (dis)connect a device in the native layers
+     * @param connect true if connection
+     * @param device the device type
+     * @param address the address of the device
+     * @param deviceName human-readable name of device
+     * @return false if an error was reported by AudioSystem
+     */
+    /*package*/ boolean handleDeviceConnection(boolean connect, int device, String address,
+            String deviceName) {
+        if (AudioService.DEBUG_DEVICES) {
+            Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:"
+                    + Integer.toHexString(device) + " address:" + address
+                    + " name:" + deviceName + ")");
+        }
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "handleDeviceConnection")
+                .set(MediaMetrics.Property.ADDRESS, address)
+                .set(MediaMetrics.Property.DEVICE, AudioSystem.getDeviceName(device))
+                .set(MediaMetrics.Property.MODE, connect
+                        ? MediaMetrics.Value.CONNECT : MediaMetrics.Value.DISCONNECT)
+                .set(MediaMetrics.Property.NAME, deviceName);
+        synchronized (mDevicesLock) {
+            final String deviceKey = DeviceInfo.makeDeviceListKey(device, address);
+            if (AudioService.DEBUG_DEVICES) {
+                Slog.i(TAG, "deviceKey:" + deviceKey);
+            }
+            DeviceInfo di = mConnectedDevices.get(deviceKey);
+            boolean isConnected = di != null;
+            if (AudioService.DEBUG_DEVICES) {
+                Slog.i(TAG, "deviceInfo:" + di + " is(already)Connected:" + isConnected);
+            }
+            if (connect && !isConnected) {
+                final int res = mAudioSystem.setDeviceConnectionState(device,
+                        AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName,
+                        AudioSystem.AUDIO_FORMAT_DEFAULT);
+                if (res != AudioSystem.AUDIO_STATUS_OK) {
+                    final String reason = "not connecting device 0x" + Integer.toHexString(device)
+                            + " due to command error " + res;
+                    Slog.e(TAG, reason);
+                    mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                            .set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED)
+                            .record();
+                    return false;
+                }
+                mConnectedDevices.put(deviceKey, new DeviceInfo(
+                        device, deviceName, address, AudioSystem.AUDIO_FORMAT_DEFAULT));
+                mDeviceBroker.postAccessoryPlugMediaUnmute(device);
+                mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
+                return true;
+            } else if (!connect && isConnected) {
+                mAudioSystem.setDeviceConnectionState(device,
+                        AudioSystem.DEVICE_STATE_UNAVAILABLE, address, deviceName,
+                        AudioSystem.AUDIO_FORMAT_DEFAULT);
+                // always remove even if disconnection failed
+                mConnectedDevices.remove(deviceKey);
+                mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
+                return true;
+            }
+            Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey
+                    + ", deviceSpec=" + di + ", connect=" + connect);
+        }
+        mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED).record();
+        return false;
+    }
+
+
+    /*package*/ void disconnectA2dp() {
+        synchronized (mDevicesLock) {
+            final ArraySet<String> toRemove = new ArraySet<>();
+            // Disconnect ALL DEVICE_OUT_BLUETOOTH_A2DP devices
+            mConnectedDevices.values().forEach(deviceInfo -> {
+                if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) {
+                    toRemove.add(deviceInfo.mDeviceAddress);
+                }
+            });
+            new MediaMetrics.Item(mMetricsId + "disconnectA2dp")
+                    .record();
+            if (toRemove.size() > 0) {
+                final int delay = checkSendBecomingNoisyIntentInt(
+                        AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                        AudioService.CONNECTION_STATE_DISCONNECTED, AudioSystem.DEVICE_NONE);
+                toRemove.stream().forEach(deviceAddress ->
+                        makeA2dpDeviceUnavailableLater(deviceAddress, delay)
+                );
+            }
+        }
+    }
+
+    /*package*/ void disconnectA2dpSink() {
+        synchronized (mDevicesLock) {
+            final ArraySet<String> toRemove = new ArraySet<>();
+            // Disconnect ALL DEVICE_IN_BLUETOOTH_A2DP devices
+            mConnectedDevices.values().forEach(deviceInfo -> {
+                if (deviceInfo.mDeviceType == AudioSystem.DEVICE_IN_BLUETOOTH_A2DP) {
+                    toRemove.add(deviceInfo.mDeviceAddress);
+                }
+            });
+            new MediaMetrics.Item(mMetricsId + "disconnectA2dpSink")
+                    .record();
+            toRemove.stream().forEach(deviceAddress -> makeA2dpSrcUnavailable(deviceAddress));
+        }
+    }
+
+    /*package*/ void disconnectHearingAid() {
+        synchronized (mDevicesLock) {
+            final ArraySet<String> toRemove = new ArraySet<>();
+            // Disconnect ALL DEVICE_OUT_HEARING_AID devices
+            mConnectedDevices.values().forEach(deviceInfo -> {
+                if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) {
+                    toRemove.add(deviceInfo.mDeviceAddress);
+                }
+            });
+            new MediaMetrics.Item(mMetricsId + "disconnectHearingAid")
+                    .record();
+            if (toRemove.size() > 0) {
+                final int delay = checkSendBecomingNoisyIntentInt(
+                        AudioSystem.DEVICE_OUT_HEARING_AID, 0, AudioSystem.DEVICE_NONE);
+                toRemove.stream().forEach(deviceAddress ->
+                        // TODO delay not used?
+                        makeHearingAidDeviceUnavailable(deviceAddress /*, delay*/)
+                );
+            }
+        }
+    }
+
+    // must be called before removing the device from mConnectedDevices
+    // musicDevice argument is used when not AudioSystem.DEVICE_NONE instead of querying
+    // from AudioSystem
+    /*package*/ int checkSendBecomingNoisyIntent(int device,
+            @AudioService.ConnectionState int state, int musicDevice) {
+        synchronized (mDevicesLock) {
+            return checkSendBecomingNoisyIntentInt(device, state, musicDevice);
+        }
+    }
+
+    /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
+        synchronized (mCurAudioRoutes) {
+            AudioRoutesInfo routes = new AudioRoutesInfo(mCurAudioRoutes);
+            mRoutesObservers.register(observer);
+            return routes;
+        }
+    }
+
+    /*package*/ AudioRoutesInfo getCurAudioRoutes() {
+        return mCurAudioRoutes;
+    }
+
+    // only public for mocking/spying
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    @VisibleForTesting
+    public void setBluetoothA2dpDeviceConnectionState(
+            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
+            int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) {
+        int delay;
+        if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) {
+            throw new IllegalArgumentException("invalid profile " + profile);
+        }
+        synchronized (mDevicesLock) {
+            if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) {
+                @AudioService.ConnectionState int asState =
+                        (state == BluetoothA2dp.STATE_CONNECTED)
+                                ? AudioService.CONNECTION_STATE_CONNECTED
+                                : AudioService.CONNECTION_STATE_DISCONNECTED;
+                delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                        asState, musicDevice);
+            } else {
+                delay = 0;
+            }
+
+            final int a2dpCodec = mDeviceBroker.getA2dpCodec(device);
+
+            if (AudioService.DEBUG_DEVICES) {
+                Log.i(TAG, "setBluetoothA2dpDeviceConnectionState device: " + device
+                        + " state: " + state + " delay(ms): " + delay
+                        + " codec:" + Integer.toHexString(a2dpCodec)
+                        + " suppressNoisyIntent: " + suppressNoisyIntent);
+            }
+
+            final BtHelper.BluetoothA2dpDeviceInfo a2dpDeviceInfo =
+                    new BtHelper.BluetoothA2dpDeviceInfo(device, a2dpVolume, a2dpCodec);
+            if (profile == BluetoothProfile.A2DP) {
+                mDeviceBroker.postA2dpSinkConnection(state,
+                            a2dpDeviceInfo,
+                            delay);
+            } else { //profile == BluetoothProfile.A2DP_SINK
+                mDeviceBroker.postA2dpSourceConnection(state,
+                        a2dpDeviceInfo,
+                        delay);
+            }
+        }
+    }
+
+    /*package*/ int setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state,
+                                                  String address, String name, String caller) {
+        synchronized (mDevicesLock) {
+            int delay = checkSendBecomingNoisyIntentInt(type, state, AudioSystem.DEVICE_NONE);
+            mDeviceBroker.postSetWiredDeviceConnectionState(
+                    new WiredDeviceConnectionState(type, state, address, name, caller),
+                    delay);
+            return delay;
+        }
+    }
+
+    /*package*/ int  setBluetoothHearingAidDeviceConnectionState(
+            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
+            boolean suppressNoisyIntent, int musicDevice) {
+        int delay;
+        synchronized (mDevicesLock) {
+            if (!suppressNoisyIntent) {
+                int intState = (state == BluetoothHearingAid.STATE_CONNECTED) ? 1 : 0;
+                delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_HEARING_AID,
+                        intState, musicDevice);
+            } else {
+                delay = 0;
+            }
+            mDeviceBroker.postSetHearingAidConnectionState(state, device, delay);
+            if (state == BluetoothHearingAid.STATE_CONNECTED) {
+                mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, AudioSystem.FORCE_NONE,
+                                "HEARING_AID set to CONNECTED");
+            }
+            return delay;
+        }
+    }
+
+
+    //-------------------------------------------------------------------
+    // Internal utilities
+
+    @GuardedBy("mDevicesLock")
+    private void makeA2dpDeviceAvailable(String address, String name, String eventSource,
+            int a2dpCodec) {
+        // enable A2DP before notifying A2DP connection to avoid unnecessary processing in
+        // audio policy manager
+        mDeviceBroker.setBluetoothA2dpOnInt(true, eventSource);
+        // at this point there could be another A2DP device already connected in APM, but it
+        // doesn't matter as this new one will overwrite the previous one
+        final int res = mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                AudioSystem.DEVICE_STATE_AVAILABLE, address, name, a2dpCodec);
+
+        // TODO: log in MediaMetrics once distinction between connection failure and
+        // double connection is made.
+        if (res != AudioSystem.AUDIO_STATUS_OK) {
+            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                    "APM failed to make available A2DP device addr=" + address
+                            + " error=" + res).printLog(TAG));
+            // TODO: connection failed, stop here
+            // TODO: return;
+        } else {
+            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                    "A2DP device addr=" + address + " now available").printLog(TAG));
+        }
+
+        // Reset A2DP suspend state each time a new sink is connected
+        mAudioSystem.setParameters("A2dpSuspended=false");
+
+        final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
+                address, a2dpCodec);
+        final String diKey = di.getKey();
+        mConnectedDevices.put(diKey, di);
+        // on a connection always overwrite the device seen by AudioPolicy, see comment above when
+        // calling AudioSystem
+        mApmConnectedDevices.put(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, diKey);
+
+        mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
+        setCurrentAudioRouteNameIfPossible(name);
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void makeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address)
+                .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(a2dpCodec))
+                .set(MediaMetrics.Property.EVENT, "makeA2dpDeviceUnavailableNow");
+
+        if (address == null) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "address null").record();
+            return;
+        }
+        final String deviceToRemoveKey =
+                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
+
+        mConnectedDevices.remove(deviceToRemoveKey);
+        if (!deviceToRemoveKey
+                .equals(mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) {
+            // removing A2DP device not currently used by AudioPolicy, log but don't act on it
+            AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
+                    "A2DP device " + address + " made unavailable, was not used")).printLog(TAG));
+            mmi.set(MediaMetrics.Property.EARLY_RETURN,
+                    "A2DP device made unavailable, was not used")
+                    .record();
+            return;
+        }
+
+        // device to remove was visible by APM, update APM
+        mDeviceBroker.setAvrcpAbsoluteVolumeSupported(false);
+        final int res = mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "", a2dpCodec);
+
+        if (res != AudioSystem.AUDIO_STATUS_OK) {
+            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                    "APM failed to make unavailable A2DP device addr=" + address
+                            + " error=" + res).printLog(TAG));
+            // TODO:  failed to disconnect, stop here
+            // TODO: return;
+        } else {
+            AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
+                    "A2DP device addr=" + address + " made unavailable")).printLog(TAG));
+        }
+        mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
+        // Remove A2DP routes as well
+        setCurrentAudioRouteNameIfPossible(null);
+        mmi.record();
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void makeA2dpDeviceUnavailableLater(String address, int delayMs) {
+        // prevent any activity on the A2DP audio output to avoid unwanted
+        // reconnection of the sink.
+        mAudioSystem.setParameters("A2dpSuspended=true");
+        // retrieve DeviceInfo before removing device
+        final String deviceKey =
+                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
+        final DeviceInfo deviceInfo = mConnectedDevices.get(deviceKey);
+        final int a2dpCodec = deviceInfo != null ? deviceInfo.mDeviceCodecFormat :
+                AudioSystem.AUDIO_FORMAT_DEFAULT;
+        // the device will be made unavailable later, so consider it disconnected right away
+        mConnectedDevices.remove(deviceKey);
+        // send the delayed message to make the device unavailable later
+        mDeviceBroker.setA2dpTimeout(address, a2dpCodec, delayMs);
+    }
+
+
+    @GuardedBy("mDevicesLock")
+    private void makeA2dpSrcAvailable(String address) {
+        mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP,
+                AudioSystem.DEVICE_STATE_AVAILABLE, address, "",
+                AudioSystem.AUDIO_FORMAT_DEFAULT);
+        mConnectedDevices.put(
+                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address),
+                new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "",
+                        address, AudioSystem.AUDIO_FORMAT_DEFAULT));
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void makeA2dpSrcUnavailable(String address) {
+        mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP,
+                AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "",
+                AudioSystem.AUDIO_FORMAT_DEFAULT);
+        mConnectedDevices.remove(
+                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void makeHearingAidDeviceAvailable(
+            String address, String name, int streamType, String eventSource) {
+        final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
+                AudioSystem.DEVICE_OUT_HEARING_AID);
+        mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
+
+        mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID,
+                AudioSystem.DEVICE_STATE_AVAILABLE, address, name,
+                AudioSystem.AUDIO_FORMAT_DEFAULT);
+        mConnectedDevices.put(
+                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address),
+                new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name,
+                        address, AudioSystem.AUDIO_FORMAT_DEFAULT));
+        mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID);
+        mDeviceBroker.postApplyVolumeOnDevice(streamType,
+                AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
+        setCurrentAudioRouteNameIfPossible(name);
+        new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable")
+                .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+                .set(MediaMetrics.Property.NAME, name)
+                .set(MediaMetrics.Property.STREAM_TYPE,
+                        AudioSystem.streamToString(streamType))
+                .record();
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void makeHearingAidDeviceUnavailable(String address) {
+        mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID,
+                AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "",
+                AudioSystem.AUDIO_FORMAT_DEFAULT);
+        mConnectedDevices.remove(
+                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address));
+        // Remove Hearing Aid routes as well
+        setCurrentAudioRouteNameIfPossible(null);
+        new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceUnavailable")
+                .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+                .record();
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void setCurrentAudioRouteNameIfPossible(String name) {
+        synchronized (mCurAudioRoutes) {
+            if (TextUtils.equals(mCurAudioRoutes.bluetoothName, name)) {
+                return;
+            }
+            if (name != null || !isCurrentDeviceConnected()) {
+                mCurAudioRoutes.bluetoothName = name;
+                mDeviceBroker.postReportNewRoutes();
+            }
+        }
+    }
+
+    @GuardedBy("mDevicesLock")
+    private boolean isCurrentDeviceConnected() {
+        return mConnectedDevices.values().stream().anyMatch(deviceInfo ->
+            TextUtils.equals(deviceInfo.mDeviceName, mCurAudioRoutes.bluetoothName));
+    }
+
+    // Devices which removal triggers intent ACTION_AUDIO_BECOMING_NOISY. The intent is only
+    // sent if:
+    // - none of these devices are connected anymore after one is disconnected AND
+    // - the device being disconnected is actually used for music.
+    // Access synchronized on mConnectedDevices
+    private static final Set<Integer> BECOMING_NOISY_INTENT_DEVICES_SET;
+    static {
+        BECOMING_NOISY_INTENT_DEVICES_SET = new HashSet<>();
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADSET);
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE);
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HDMI);
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET);
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET);
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_LINE);
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HEARING_AID);
+        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET);
+    }
+
+    // must be called before removing the device from mConnectedDevices
+    // musicDevice argument is used when not AudioSystem.DEVICE_NONE instead of querying
+    // from AudioSystem
+    @GuardedBy("mDevicesLock")
+    private int checkSendBecomingNoisyIntentInt(int device,
+            @AudioService.ConnectionState int state, int musicDevice) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
+                + "checkSendBecomingNoisyIntentInt")
+                .set(MediaMetrics.Property.DEVICE, AudioSystem.getDeviceName(device))
+                .set(MediaMetrics.Property.STATE,
+                        state == AudioService.CONNECTION_STATE_CONNECTED
+                                ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED);
+        if (state != AudioService.CONNECTION_STATE_DISCONNECTED) {
+            mmi.set(MediaMetrics.Property.DELAY_MS, 0).record(); // OK to return
+            return 0;
+        }
+        if (!BECOMING_NOISY_INTENT_DEVICES_SET.contains(device)) {
+            mmi.set(MediaMetrics.Property.DELAY_MS, 0).record(); // OK to return
+            return 0;
+        }
+        int delay = 0;
+        Set<Integer> devices = new HashSet<>();
+        for (DeviceInfo di : mConnectedDevices.values()) {
+            if (((di.mDeviceType & AudioSystem.DEVICE_BIT_IN) == 0)
+                    && BECOMING_NOISY_INTENT_DEVICES_SET.contains(di.mDeviceType)) {
+                devices.add(di.mDeviceType);
+            }
+        }
+        if (musicDevice == AudioSystem.DEVICE_NONE) {
+            musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC);
+        }
+
+        // always ignore condition on device being actually used for music when in communication
+        // because music routing is altered in this case.
+        // also checks whether media routing if affected by a dynamic policy or mirroring
+        if (((device == musicDevice) || mDeviceBroker.isInCommunication())
+                && AudioSystem.isSingleAudioDeviceType(devices, device)
+                && !mDeviceBroker.hasMediaDynamicPolicy()
+                && (musicDevice != AudioSystem.DEVICE_OUT_REMOTE_SUBMIX)) {
+            if (!mAudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0 /*not looking in past*/)
+                    && !mDeviceBroker.hasAudioFocusUsers()) {
+                // no media playback, not a "becoming noisy" situation, otherwise it could cause
+                // the pausing of some apps that are playing remotely
+                AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
+                        "dropping ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG));
+                mmi.set(MediaMetrics.Property.DELAY_MS, 0).record(); // OK to return
+                return 0;
+            }
+            mDeviceBroker.postBroadcastBecomingNoisy();
+            delay = AudioService.BECOMING_NOISY_DELAY_MS;
+        }
+
+        mmi.set(MediaMetrics.Property.DELAY_MS, delay).record();
+        return delay;
+    }
+
+    // Intent "extra" data keys.
+    private static final String CONNECT_INTENT_KEY_PORT_NAME = "portName";
+    private static final String CONNECT_INTENT_KEY_STATE = "state";
+    private static final String CONNECT_INTENT_KEY_ADDRESS = "address";
+    private static final String CONNECT_INTENT_KEY_HAS_PLAYBACK = "hasPlayback";
+    private static final String CONNECT_INTENT_KEY_HAS_CAPTURE = "hasCapture";
+    private static final String CONNECT_INTENT_KEY_HAS_MIDI = "hasMIDI";
+    private static final String CONNECT_INTENT_KEY_DEVICE_CLASS = "class";
+
+    private void sendDeviceConnectionIntent(int device, int state, String address,
+                                            String deviceName) {
+        if (AudioService.DEBUG_DEVICES) {
+            Slog.i(TAG, "sendDeviceConnectionIntent(dev:0x" + Integer.toHexString(device)
+                    + " state:0x" + Integer.toHexString(state) + " address:" + address
+                    + " name:" + deviceName + ");");
+        }
+        Intent intent = new Intent();
+
+        switch(device) {
+            case AudioSystem.DEVICE_OUT_WIRED_HEADSET:
+                intent.setAction(Intent.ACTION_HEADSET_PLUG);
+                intent.putExtra("microphone", 1);
+                break;
+            case AudioSystem.DEVICE_OUT_WIRED_HEADPHONE:
+            case AudioSystem.DEVICE_OUT_LINE:
+                intent.setAction(Intent.ACTION_HEADSET_PLUG);
+                intent.putExtra("microphone", 0);
+                break;
+            case AudioSystem.DEVICE_OUT_USB_HEADSET:
+                intent.setAction(Intent.ACTION_HEADSET_PLUG);
+                intent.putExtra("microphone",
+                        AudioSystem.getDeviceConnectionState(AudioSystem.DEVICE_IN_USB_HEADSET, "")
+                                == AudioSystem.DEVICE_STATE_AVAILABLE ? 1 : 0);
+                break;
+            case AudioSystem.DEVICE_IN_USB_HEADSET:
+                if (AudioSystem.getDeviceConnectionState(AudioSystem.DEVICE_OUT_USB_HEADSET, "")
+                        == AudioSystem.DEVICE_STATE_AVAILABLE) {
+                    intent.setAction(Intent.ACTION_HEADSET_PLUG);
+                    intent.putExtra("microphone", 1);
+                } else {
+                    // do not send ACTION_HEADSET_PLUG when only the input side is seen as changing
+                    return;
+                }
+                break;
+            case AudioSystem.DEVICE_OUT_HDMI:
+            case AudioSystem.DEVICE_OUT_HDMI_ARC:
+                configureHdmiPlugIntent(intent, state);
+                break;
+        }
+
+        if (intent.getAction() == null) {
+            return;
+        }
+
+        intent.putExtra(CONNECT_INTENT_KEY_STATE, state);
+        intent.putExtra(CONNECT_INTENT_KEY_ADDRESS, address);
+        intent.putExtra(CONNECT_INTENT_KEY_PORT_NAME, deviceName);
+
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_CURRENT);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void updateAudioRoutes(int device, int state) {
+        int connType = 0;
+
+        switch (device) {
+            case AudioSystem.DEVICE_OUT_WIRED_HEADSET:
+                connType = AudioRoutesInfo.MAIN_HEADSET;
+                break;
+            case AudioSystem.DEVICE_OUT_WIRED_HEADPHONE:
+            case AudioSystem.DEVICE_OUT_LINE:
+                connType = AudioRoutesInfo.MAIN_HEADPHONES;
+                break;
+            case AudioSystem.DEVICE_OUT_HDMI:
+            case AudioSystem.DEVICE_OUT_HDMI_ARC:
+                connType = AudioRoutesInfo.MAIN_HDMI;
+                break;
+            case AudioSystem.DEVICE_OUT_USB_DEVICE:
+            case AudioSystem.DEVICE_OUT_USB_HEADSET:
+                connType = AudioRoutesInfo.MAIN_USB;
+                break;
+        }
+
+        synchronized (mCurAudioRoutes) {
+            if (connType == 0) {
+                return;
+            }
+            int newConn = mCurAudioRoutes.mainType;
+            if (state != 0) {
+                newConn |= connType;
+            } else {
+                newConn &= ~connType;
+            }
+            if (newConn != mCurAudioRoutes.mainType) {
+                mCurAudioRoutes.mainType = newConn;
+                mDeviceBroker.postReportNewRoutes();
+            }
+        }
+    }
+
+    private void configureHdmiPlugIntent(Intent intent, @AudioService.ConnectionState int state) {
+        intent.setAction(AudioManager.ACTION_HDMI_AUDIO_PLUG);
+        intent.putExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, state);
+        if (state != AudioService.CONNECTION_STATE_CONNECTED) {
+            return;
+        }
+        ArrayList<AudioPort> ports = new ArrayList<AudioPort>();
+        int[] portGeneration = new int[1];
+        int status = AudioSystem.listAudioPorts(ports, portGeneration);
+        if (status != AudioManager.SUCCESS) {
+            Log.e(TAG, "listAudioPorts error " + status + " in configureHdmiPlugIntent");
+            return;
+        }
+        for (AudioPort port : ports) {
+            if (!(port instanceof AudioDevicePort)) {
+                continue;
+            }
+            final AudioDevicePort devicePort = (AudioDevicePort) port;
+            if (devicePort.type() != AudioManager.DEVICE_OUT_HDMI
+                    && devicePort.type() != AudioManager.DEVICE_OUT_HDMI_ARC) {
+                continue;
+            }
+            // found an HDMI port: format the list of supported encodings
+            int[] formats = AudioFormat.filterPublicFormats(devicePort.formats());
+            if (formats.length > 0) {
+                ArrayList<Integer> encodingList = new ArrayList(1);
+                for (int format : formats) {
+                    // a format in the list can be 0, skip it
+                    if (format != AudioFormat.ENCODING_INVALID) {
+                        encodingList.add(format);
+                    }
+                }
+                final int[] encodingArray = encodingList.stream().mapToInt(i -> i).toArray();
+                intent.putExtra(AudioManager.EXTRA_ENCODINGS, encodingArray);
+            }
+            // find the maximum supported number of channels
+            int maxChannels = 0;
+            for (int mask : devicePort.channelMasks()) {
+                int channelCount = AudioFormat.channelCountFromOutChannelMask(mask);
+                if (channelCount > maxChannels) {
+                    maxChannels = channelCount;
+                }
+            }
+            intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, maxChannels);
+        }
+    }
+
+    private void dispatchPreferredDevice(int strategy, @Nullable AudioDeviceAttributes device) {
+        final int nbDispatchers = mPrefDevDispatchers.beginBroadcast();
+        for (int i = 0; i < nbDispatchers; i++) {
+            try {
+                mPrefDevDispatchers.getBroadcastItem(i).dispatchPrefDeviceChanged(strategy, device);
+            } catch (RemoteException e) {
+            }
+        }
+        mPrefDevDispatchers.finishBroadcast();
+    }
+
+    //----------------------------------------------------------
+    // 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 (mDevicesLock) {
+            return (mConnectedDevices.get(key) != null);
+        }
+    }
+}
diff --git a/com/android/server/audio/AudioEventLogger.java b/com/android/server/audio/AudioEventLogger.java
new file mode 100644
index 0000000..9ebd75b
--- /dev/null
+++ b/com/android/server/audio/AudioEventLogger.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 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 android.util.Log;
+
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+
+public class AudioEventLogger {
+
+    // ring buffer of events to log.
+    private final LinkedList<Event> mEvents;
+
+    private final String mTitle;
+
+    // the maximum number of events to keep in log
+    private final int mMemSize;
+
+    public static abstract class Event {
+        // formatter for timestamps
+        private final static SimpleDateFormat sFormat = new SimpleDateFormat("MM-dd HH:mm:ss:SSS");
+
+        private final long mTimestamp;
+
+        Event() {
+            mTimestamp = System.currentTimeMillis();
+        }
+
+        public String toString() {
+            return (new StringBuilder(sFormat.format(new Date(mTimestamp))))
+                    .append(" ").append(eventToString()).toString();
+        }
+
+        /**
+         * Causes the string message for the event to appear in the logcat.
+         * Here is an example of how to create a new event (a StringEvent), adding it to the logger
+         * (an instance of AudioEventLogger) while also making it show in the logcat:
+         * <pre>
+         *     myLogger.log(
+         *         (new StringEvent("something for logcat and logger")).printLog(MyClass.TAG) );
+         * </pre>
+         * @param tag the tag for the android.util.Log.v
+         * @return the same instance of the event
+         */
+        public Event printLog(String tag) {
+            Log.i(tag, eventToString());
+            return this;
+        }
+
+        /**
+         * Convert event to String.
+         * This method is only called when the logger history is about to the dumped,
+         * so this method is where expensive String conversions should be made, not when the Event
+         * subclass is created.
+         * Timestamp information will be automatically added, do not include it.
+         * @return a string representation of the event that occurred.
+         */
+        abstract public String eventToString();
+    }
+
+    public static class StringEvent extends Event {
+        private final String mMsg;
+
+        public StringEvent(String msg) {
+            mMsg = msg;
+        }
+
+        @Override
+        public String eventToString() {
+            return mMsg;
+        }
+    }
+
+    /**
+     * Constructor for logger.
+     * @param size the maximum number of events to keep in log
+     * @param title the string displayed before the recorded log
+     */
+    public AudioEventLogger(int size, String title) {
+        mEvents = new LinkedList<Event>();
+        mMemSize = size;
+        mTitle = title;
+    }
+
+    public synchronized void log(Event evt) {
+        if (mEvents.size() >= mMemSize) {
+            mEvents.removeFirst();
+        }
+        mEvents.add(evt);
+    }
+
+    public synchronized void dump(PrintWriter pw) {
+        pw.println("Audio event log: " + mTitle);
+        for (Event evt : mEvents) {
+            pw.println(evt.toString());
+        }
+    }
+}
diff --git a/com/android/server/audio/AudioService.java b/com/android/server/audio/AudioService.java
new file mode 100644
index 0000000..7cac376
--- /dev/null
+++ b/com/android/server/audio/AudioService.java
@@ -0,0 +1,8708 @@
+/*
+ * Copyright (C) 2006 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 android.Manifest.permission.REMOTE_AUDIO_PLAYBACK;
+import static android.media.AudioManager.RINGER_MODE_NORMAL;
+import static android.media.AudioManager.RINGER_MODE_SILENT;
+import static android.media.AudioManager.RINGER_MODE_VIBRATE;
+import static android.media.AudioManager.STREAM_SYSTEM;
+import static android.os.Process.FIRST_APPLICATION_UID;
+import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE;
+import static android.provider.Settings.Secure.VOLUME_HUSH_OFF;
+import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.IUidObserver;
+import android.app.NotificationManager;
+import android.app.role.OnRoleHoldersChangedListener;
+import android.app.role.RoleManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.database.ContentObserver;
+import android.hardware.hdmi.HdmiAudioSystemClient;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiPlaybackClient;
+import android.hardware.hdmi.HdmiTvClient;
+import android.hardware.input.InputManager;
+import android.hardware.usb.UsbManager;
+import android.hidl.manager.V1_0.IServiceManager;
+import android.media.AudioAttributes;
+import android.media.AudioAttributes.AttributeSystemUsage;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioFocusInfo;
+import android.media.AudioFocusRequest;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioManagerInternal;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioRecordingConfiguration;
+import android.media.AudioRoutesInfo;
+import android.media.AudioSystem;
+import android.media.IAudioFocusDispatcher;
+import android.media.IAudioRoutesObserver;
+import android.media.IAudioServerStateDispatcher;
+import android.media.IAudioService;
+import android.media.IPlaybackConfigDispatcher;
+import android.media.IRecordingConfigDispatcher;
+import android.media.IRingtonePlayer;
+import android.media.IStrategyPreferredDeviceDispatcher;
+import android.media.IVolumeController;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMetrics;
+import android.media.PlayerBase;
+import android.media.VolumePolicy;
+import android.media.audiofx.AudioEffect;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioPolicy;
+import android.media.audiopolicy.AudioPolicyConfig;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.media.audiopolicy.AudioVolumeGroup;
+import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.projection.IMediaProjection;
+import android.media.projection.IMediaProjectionCallback;
+import android.media.projection.IMediaProjectionManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.UserManagerInternal;
+import android.os.UserManagerInternal.UserRestrictionsListener;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.provider.Settings.System;
+import android.service.notification.ZenModeConfig;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.PrintWriterPrinter;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityManager;
+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;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.audio.AudioServiceEvents.PhoneStateEvent;
+import com.android.server.audio.AudioServiceEvents.VolumeEvent;
+import com.android.server.pm.UserManagerService;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * The implementation of the audio service for volume, audio focus, device management...
+ * <p>
+ * This implementation focuses on delivering a responsive UI. Most methods are
+ * asynchronous to external calls. For example, the task of setting a volume
+ * will update our internal state, but in a separate thread will set the system
+ * volume and later persist to the database. Similarly, setting the ringer mode
+ * will update the state and broadcast a change and in a separate thread later
+ * persist the ringer mode.
+ *
+ * @hide
+ */
+public class AudioService extends IAudioService.Stub
+        implements AccessibilityManager.TouchExplorationStateChangeListener,
+            AccessibilityManager.AccessibilityServicesStateChangeListener {
+
+    private static final String TAG = "AS.AudioService";
+
+    private final AudioSystemAdapter mAudioSystem;
+    private final SystemServerAdapter mSystemServer;
+
+    /** Debug audio mode */
+    protected static final boolean DEBUG_MODE = false;
+
+    /** Debug audio policy feature */
+    protected static final boolean DEBUG_AP = false;
+
+    /** Debug volumes */
+    protected static final boolean DEBUG_VOL = false;
+
+    /** debug calls to devices APIs */
+    protected static final boolean DEBUG_DEVICES = false;
+
+    /** How long to delay before persisting a change in volume/ringer mode. */
+    private static final int PERSIST_DELAY = 500;
+
+    /** How long to delay after a volume down event before unmuting a stream */
+    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;
+
+    private final Context mContext;
+    private final ContentResolver mContentResolver;
+    private final AppOpsManager mAppOps;
+
+    // the platform type affects volume and silent mode behavior
+    private final int mPlatformType;
+
+    // indicates whether the system maps all streams to a single stream.
+    private final boolean mIsSingleVolume;
+
+    private boolean isPlatformVoice() {
+        return mPlatformType == AudioSystem.PLATFORM_VOICE;
+    }
+
+    /*package*/ boolean isPlatformTelevision() {
+        return mPlatformType == AudioSystem.PLATFORM_TELEVISION;
+    }
+
+    /*package*/ boolean isPlatformAutomotive() {
+        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    /** The controller for the volume UI. */
+    private final VolumeController mVolumeController = new VolumeController();
+
+    // sendMsg() flags
+    /** If the msg is already queued, replace it with this one. */
+    private static final int SENDMSG_REPLACE = 0;
+    /** If the msg is already queued, ignore this one and leave the old. */
+    private static final int SENDMSG_NOOP = 1;
+    /** If the msg is already queued, queue this one and leave the old. */
+    private static final int SENDMSG_QUEUE = 2;
+
+    // AudioHandler messages
+    private static final int MSG_SET_DEVICE_VOLUME = 0;
+    private static final int MSG_PERSIST_VOLUME = 1;
+    private static final int MSG_PERSIST_VOLUME_GROUP = 2;
+    private static final int MSG_PERSIST_RINGER_MODE = 3;
+    private static final int MSG_AUDIO_SERVER_DIED = 4;
+    private static final int MSG_PLAY_SOUND_EFFECT = 5;
+    private static final int MSG_LOAD_SOUND_EFFECTS = 7;
+    private static final int MSG_SET_FORCE_USE = 8;
+    private static final int MSG_BT_HEADSET_CNCT_FAILED = 9;
+    private static final int MSG_SET_ALL_VOLUMES = 10;
+    private static final int MSG_CHECK_MUSIC_ACTIVE = 11;
+    private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME = 12;
+    private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED = 13;
+    private static final int MSG_PERSIST_SAFE_VOLUME_STATE = 14;
+    private static final int MSG_UNLOAD_SOUND_EFFECTS = 15;
+    private static final int MSG_SYSTEM_READY = 16;
+    private static final int MSG_PERSIST_MUSIC_ACTIVE_MS = 17;
+    private static final int MSG_UNMUTE_STREAM = 18;
+    private static final int MSG_DYN_POLICY_MIX_STATE_UPDATE = 19;
+    private static final int MSG_INDICATE_SYSTEM_READY = 20;
+    private static final int MSG_ACCESSORY_PLUG_MEDIA_UNMUTE = 21;
+    private static final int MSG_NOTIFY_VOL_EVENT = 22;
+    private static final int MSG_DISPATCH_AUDIO_SERVER_STATE = 23;
+    private static final int MSG_ENABLE_SURROUND_FORMATS = 24;
+    private static final int MSG_UPDATE_RINGER_MODE = 25;
+    private static final int MSG_SET_DEVICE_STREAM_VOLUME = 26;
+    private static final int MSG_OBSERVE_DEVICES_FOR_ALL_STREAMS = 27;
+    private static final int MSG_HDMI_VOLUME_CHECK = 28;
+    private static final int MSG_PLAYBACK_CONFIG_CHANGE = 29;
+    private static final int MSG_BROADCAST_MICROPHONE_MUTE = 30;
+    // start of messages handled under wakelock
+    //   these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
+    //   and not with sendMsg(..., ..., SENDMSG_QUEUE, ...)
+    private static final int MSG_DISABLE_AUDIO_FOR_UID = 100;
+    // end of messages handled under wakelock
+
+    // retry delay in case of failure to indicate system ready to AudioFlinger
+    private static final int INDICATE_SYSTEM_READY_RETRY_DELAY_MS = 1000;
+
+    /** @see AudioSystemThread */
+    private AudioSystemThread mAudioSystemThread;
+    /** @see AudioHandler */
+    private AudioHandler mAudioHandler;
+    /** @see VolumeStreamState */
+    private VolumeStreamState[] mStreamStates;
+
+    /*package*/ int getVssVolumeForDevice(int stream, int device) {
+        return mStreamStates[stream].getIndex(device);
+    }
+
+    private SettingsObserver mSettingsObserver;
+
+    private int mMode = AudioSystem.MODE_NORMAL;
+    // protects mRingerMode
+    private final Object mSettingsLock = new Object();
+
+   /** Maximum volume index values for audio streams */
+    protected static int[] MAX_STREAM_VOLUME = new int[] {
+        5,  // STREAM_VOICE_CALL
+        7,  // STREAM_SYSTEM
+        7,  // STREAM_RING
+        15, // STREAM_MUSIC
+        7,  // STREAM_ALARM
+        7,  // STREAM_NOTIFICATION
+        15, // STREAM_BLUETOOTH_SCO
+        7,  // STREAM_SYSTEM_ENFORCED
+        15, // STREAM_DTMF
+        15, // STREAM_TTS
+        15, // STREAM_ACCESSIBILITY
+        15  // STREAM_ASSISTANT
+    };
+
+    /** Minimum volume index values for audio streams */
+    protected static int[] MIN_STREAM_VOLUME = new int[] {
+        1,  // STREAM_VOICE_CALL
+        0,  // STREAM_SYSTEM
+        0,  // STREAM_RING
+        0,  // STREAM_MUSIC
+        1,  // STREAM_ALARM
+        0,  // STREAM_NOTIFICATION
+        0,  // STREAM_BLUETOOTH_SCO
+        0,  // STREAM_SYSTEM_ENFORCED
+        0,  // STREAM_DTMF
+        0,  // STREAM_TTS
+        1,  // STREAM_ACCESSIBILITY
+        0   // STREAM_ASSISTANT
+    };
+
+    /* mStreamVolumeAlias[] indicates for each stream if it uses the volume settings
+     * of another stream: This avoids multiplying the volume settings for hidden
+     * stream types that follow other stream behavior for volume settings
+     * NOTE: do not create loops in aliases!
+     * Some streams alias to different streams according to device category (phone or tablet) or
+     * use case (in call vs off call...). See updateStreamVolumeAlias() for more details.
+     *  mStreamVolumeAlias contains STREAM_VOLUME_ALIAS_VOICE aliases for a voice capable device
+     *  (phone), STREAM_VOLUME_ALIAS_TELEVISION for a television or set-top box and
+     *  STREAM_VOLUME_ALIAS_DEFAULT for other devices (e.g. tablets).*/
+    private final int[] STREAM_VOLUME_ALIAS_VOICE = new int[] {
+        AudioSystem.STREAM_VOICE_CALL,      // STREAM_VOICE_CALL
+        AudioSystem.STREAM_RING,            // STREAM_SYSTEM
+        AudioSystem.STREAM_RING,            // STREAM_RING
+        AudioSystem.STREAM_MUSIC,           // STREAM_MUSIC
+        AudioSystem.STREAM_ALARM,           // STREAM_ALARM
+        AudioSystem.STREAM_RING,            // STREAM_NOTIFICATION
+        AudioSystem.STREAM_BLUETOOTH_SCO,   // STREAM_BLUETOOTH_SCO
+        AudioSystem.STREAM_RING,            // STREAM_SYSTEM_ENFORCED
+        AudioSystem.STREAM_RING,            // STREAM_DTMF
+        AudioSystem.STREAM_MUSIC,           // STREAM_TTS
+        AudioSystem.STREAM_MUSIC,           // STREAM_ACCESSIBILITY
+        AudioSystem.STREAM_MUSIC            // STREAM_ASSISTANT
+    };
+    private final int[] STREAM_VOLUME_ALIAS_TELEVISION = new int[] {
+        AudioSystem.STREAM_MUSIC,       // STREAM_VOICE_CALL
+        AudioSystem.STREAM_MUSIC,       // STREAM_SYSTEM
+        AudioSystem.STREAM_MUSIC,       // STREAM_RING
+        AudioSystem.STREAM_MUSIC,       // STREAM_MUSIC
+        AudioSystem.STREAM_MUSIC,       // STREAM_ALARM
+        AudioSystem.STREAM_MUSIC,       // STREAM_NOTIFICATION
+        AudioSystem.STREAM_BLUETOOTH_SCO,       // STREAM_BLUETOOTH_SCO
+        AudioSystem.STREAM_MUSIC,       // STREAM_SYSTEM_ENFORCED
+        AudioSystem.STREAM_MUSIC,       // STREAM_DTMF
+        AudioSystem.STREAM_MUSIC,       // STREAM_TTS
+        AudioSystem.STREAM_MUSIC,       // STREAM_ACCESSIBILITY
+        AudioSystem.STREAM_MUSIC        // STREAM_ASSISTANT
+    };
+    private final int[] STREAM_VOLUME_ALIAS_DEFAULT = new int[] {
+        AudioSystem.STREAM_VOICE_CALL,      // STREAM_VOICE_CALL
+        AudioSystem.STREAM_RING,            // STREAM_SYSTEM
+        AudioSystem.STREAM_RING,            // STREAM_RING
+        AudioSystem.STREAM_MUSIC,           // STREAM_MUSIC
+        AudioSystem.STREAM_ALARM,           // STREAM_ALARM
+        AudioSystem.STREAM_RING,            // STREAM_NOTIFICATION
+        AudioSystem.STREAM_BLUETOOTH_SCO,   // STREAM_BLUETOOTH_SCO
+        AudioSystem.STREAM_RING,            // STREAM_SYSTEM_ENFORCED
+        AudioSystem.STREAM_RING,            // STREAM_DTMF
+        AudioSystem.STREAM_MUSIC,           // STREAM_TTS
+        AudioSystem.STREAM_MUSIC,           // STREAM_ACCESSIBILITY
+        AudioSystem.STREAM_MUSIC            // STREAM_ASSISTANT
+    };
+    protected static int[] mStreamVolumeAlias;
+
+    /**
+     * Map AudioSystem.STREAM_* constants to app ops.  This should be used
+     * after mapping through mStreamVolumeAlias.
+     */
+    private static final int[] STREAM_VOLUME_OPS = new int[] {
+        AppOpsManager.OP_AUDIO_VOICE_VOLUME,            // STREAM_VOICE_CALL
+        AppOpsManager.OP_AUDIO_MEDIA_VOLUME,            // STREAM_SYSTEM
+        AppOpsManager.OP_AUDIO_RING_VOLUME,             // STREAM_RING
+        AppOpsManager.OP_AUDIO_MEDIA_VOLUME,            // STREAM_MUSIC
+        AppOpsManager.OP_AUDIO_ALARM_VOLUME,            // STREAM_ALARM
+        AppOpsManager.OP_AUDIO_NOTIFICATION_VOLUME,     // STREAM_NOTIFICATION
+        AppOpsManager.OP_AUDIO_BLUETOOTH_VOLUME,        // STREAM_BLUETOOTH_SCO
+        AppOpsManager.OP_AUDIO_MEDIA_VOLUME,            // STREAM_SYSTEM_ENFORCED
+        AppOpsManager.OP_AUDIO_MEDIA_VOLUME,            // STREAM_DTMF
+        AppOpsManager.OP_AUDIO_MEDIA_VOLUME,            // STREAM_TTS
+        AppOpsManager.OP_AUDIO_ACCESSIBILITY_VOLUME,    // STREAM_ACCESSIBILITY
+        AppOpsManager.OP_AUDIO_MEDIA_VOLUME             // STREAM_ASSISTANT
+    };
+
+    private final boolean mUseFixedVolume;
+
+    /**
+    * Default stream type used for volume control in the absence of playback
+    * e.g. user on homescreen, no app playing anything, presses hardware volume buttons, this
+    *    stream type is controlled.
+    */
+    protected static final int DEFAULT_VOL_STREAM_NO_PLAYBACK = AudioSystem.STREAM_MUSIC;
+
+    private final AudioSystem.ErrorCallback mAudioSystemCallback = new AudioSystem.ErrorCallback() {
+        public void onError(int error) {
+            switch (error) {
+                case AudioSystem.AUDIO_STATUS_SERVER_DIED:
+                    // check for null in case error callback is called during instance creation
+                    if (mRecordMonitor != null) {
+                        mRecordMonitor.onAudioServerDied();
+                    }
+                    sendMsg(mAudioHandler, MSG_AUDIO_SERVER_DIED,
+                            SENDMSG_NOOP, 0, 0, null, 0);
+                    sendMsg(mAudioHandler, MSG_DISPATCH_AUDIO_SERVER_STATE,
+                            SENDMSG_QUEUE, 0, 0, null, 0);
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+    /**
+     * Current ringer mode from one of {@link AudioManager#RINGER_MODE_NORMAL},
+     * {@link AudioManager#RINGER_MODE_SILENT}, or
+     * {@link AudioManager#RINGER_MODE_VIBRATE}.
+     */
+    @GuardedBy("mSettingsLock")
+    private int mRingerMode;  // internal ringer mode, affects muting of underlying streams
+    @GuardedBy("mSettingsLock")
+    private int mRingerModeExternal = -1;  // reported ringer mode to outside clients (AudioManager)
+
+    /** @see System#MODE_RINGER_STREAMS_AFFECTED */
+    private int mRingerModeAffectedStreams = 0;
+
+    private int mZenModeAffectedStreams = 0;
+
+    // Streams currently muted by ringer mode and dnd
+    private int mRingerAndZenModeMutedStreams;
+
+    /** Streams that can be muted. Do not resolve to aliases when checking.
+     * @see System#MUTE_STREAMS_AFFECTED */
+    private int mMuteAffectedStreams;
+
+    @NonNull
+    private SoundEffectsHelper mSfxHelper;
+
+    /**
+     * NOTE: setVibrateSetting(), getVibrateSetting(), shouldVibrate() are deprecated.
+     * mVibrateSetting is just maintained during deprecation period but vibration policy is
+     * now only controlled by mHasVibrator and mRingerMode
+     */
+    private int mVibrateSetting;
+
+    // Is there a vibrator
+    private final boolean mHasVibrator;
+    // Used to play vibrations
+    private Vibrator mVibrator;
+    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+            .build();
+
+    // Broadcast receiver for device connections intent broadcasts
+    private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
+
+    private IMediaProjectionManager mProjectionService; // to validate projection token
+
+    /** Interface for UserManagerService. */
+    private final UserManagerInternal mUserManagerInternal;
+    private final ActivityManagerInternal mActivityManagerInternal;
+
+    private final UserRestrictionsListener mUserRestrictionsListener =
+            new AudioServiceUserRestrictionsListener();
+
+    // List of binder death handlers for setMode() client processes.
+    // The last process to have called setMode() is at the top of the list.
+    // package-private so it can be accessed in AudioDeviceBroker.getSetModeDeathHandlers
+    //TODO candidate to be moved to separate class that handles synchronization
+    @GuardedBy("mDeviceBroker.mSetModeLock")
+    /*package*/ final ArrayList<SetModeDeathHandler> mSetModeDeathHandlers =
+            new ArrayList<SetModeDeathHandler>();
+
+    // true if boot sequence has been completed
+    private boolean mSystemReady;
+    // true if Intent.ACTION_USER_SWITCHED has ever been received
+    private boolean mUserSwitchedReceived;
+    // previous volume adjustment direction received by checkForRingerModeChange()
+    private int mPrevVolDirection = AudioManager.ADJUST_SAME;
+    // mVolumeControlStream is set by VolumePanel to temporarily force the stream type which volume
+    // is controlled by Vol keys.
+    private int mVolumeControlStream = -1;
+    // interpretation of whether the volume stream has been selected by the user by clicking on a
+    // volume slider to change which volume is controlled by the volume keys. Is false
+    // when mVolumeControlStream is -1.
+    private boolean mUserSelectedVolumeControlStream = false;
+    private final Object mForceControlStreamLock = new Object();
+    // VolumePanel is currently the only client of forceVolumeControlStream() and runs in system
+    // server process so in theory it is not necessary to monitor the client death.
+    // However it is good to be ready for future evolutions.
+    private ForceControlStreamClient mForceControlStreamClient = null;
+    // Used to play ringtones outside system_server
+    private volatile IRingtonePlayer mRingtonePlayer;
+
+    // Devices for which the volume is fixed (volume is either max or muted)
+    Set<Integer> mFixedVolumeDevices = new HashSet<>(Arrays.asList(
+            AudioSystem.DEVICE_OUT_HDMI,
+            AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET,
+            AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET,
+            AudioSystem.DEVICE_OUT_HDMI_ARC,
+            AudioSystem.DEVICE_OUT_SPDIF,
+            AudioSystem.DEVICE_OUT_AUX_LINE));
+    // Devices for which the volume is always max, no volume panel
+    Set<Integer> mFullVolumeDevices = new HashSet<>();
+    // Devices for the which use the "absolute volume" concept (framework sends audio signal
+    // full scale, and volume control separately) and can be used for multiple use cases reflected
+    // by the audio mode (e.g. media playback in MODE_NORMAL, and phone calls in MODE_IN_CALL).
+    Set<Integer> mAbsVolumeMultiModeCaseDevices = new HashSet<>(
+            Arrays.asList(AudioSystem.DEVICE_OUT_HEARING_AID));
+
+    private final boolean mMonitorRotation;
+
+    private boolean mDockAudioMediaEnabled = true;
+
+    private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
+
+    // Used when safe volume warning message display is requested by setStreamVolume(). In this
+    // case, the new requested volume, stream type and device are stored in mPendingVolumeCommand
+    // and used later when/if disableSafeMediaVolume() is called.
+    private StreamVolumeCommand mPendingVolumeCommand;
+
+    private PowerManager.WakeLock mAudioEventWakeLock;
+
+    private final MediaFocusControl mMediaFocusControl;
+
+    // Pre-scale for Bluetooth Absolute Volume
+    private float[] mPrescaleAbsoluteVolume = new float[] {
+        0.5f,    // Pre-scale for index 1
+        0.7f,    // Pre-scale for index 2
+        0.85f,   // Pre-scale for index 3
+    };
+
+    private NotificationManager mNm;
+    private AudioManagerInternal.RingerModeDelegate mRingerModeDelegate;
+    private VolumePolicy mVolumePolicy = VolumePolicy.DEFAULT;
+    private long mLoweredFromNormalToVibrateTime;
+
+    // Array of Uids of valid accessibility services to check if caller is one of them
+    private int[] mAccessibilityServiceUids;
+    private final Object mAccessibilityServiceUidsLock = new Object();
+
+    private int mEncodedSurroundMode;
+    private String mEnabledSurroundFormats;
+    private boolean mSurroundModeChanged;
+
+    private boolean mMicMuteFromSwitch;
+    private boolean mMicMuteFromApi;
+    private boolean mMicMuteFromRestrictions;
+    // caches the value returned by AudioSystem.isMicrophoneMuted()
+    private boolean mMicMuteFromSystemCached;
+
+    @GuardedBy("mSettingsLock")
+    private int mAssistantUid;
+
+    @GuardedBy("mSettingsLock")
+    private int mCurrentImeUid;
+
+    private final Object mSupportedSystemUsagesLock = new Object();
+    @GuardedBy("mSupportedSystemUsagesLock")
+    private @AttributeSystemUsage int[] mSupportedSystemUsages =
+            new int[]{AudioAttributes.USAGE_CALL_ASSISTANT};
+
+    // Defines the format for the connection "address" for ALSA devices
+    public static String makeAlsaAddressString(int card, int device) {
+        return "card=" + card + ";device=" + device + ";";
+    }
+
+    public static final class Lifecycle extends SystemService {
+        private AudioService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+            mService = new AudioService(context);
+        }
+
+        @Override
+        public void onStart() {
+            publishBinderService(Context.AUDIO_SERVICE, mService);
+        }
+
+        @Override
+        public void onBootPhase(int phase) {
+            if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+                mService.systemReady();
+            }
+        }
+    }
+
+    final private IUidObserver mUidObserver = new IUidObserver.Stub() {
+        @Override public void onUidStateChanged(int uid, int procState, long procStateSeq,
+            int capability) {
+        }
+
+        @Override public void onUidGone(int uid, boolean disabled) {
+            // Once the uid is no longer running, no need to keep trying to disable its audio.
+            disableAudioForUid(false, uid);
+        }
+
+        @Override public void onUidActive(int uid) throws RemoteException {
+        }
+
+        @Override public void onUidIdle(int uid, boolean disabled) {
+        }
+
+        @Override public void onUidCachedChanged(int uid, boolean cached) {
+            disableAudioForUid(cached, uid);
+        }
+
+        private void disableAudioForUid(boolean disable, int uid) {
+            queueMsgUnderWakeLock(mAudioHandler, MSG_DISABLE_AUDIO_FOR_UID,
+                    disable ? 1 : 0 /* arg1 */,  uid /* arg2 */,
+                    null /* obj */,  0 /* delay */);
+        }
+    };
+
+    @GuardedBy("mSettingsLock")
+    private boolean mRttEnabled = false;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Construction
+    ///////////////////////////////////////////////////////////////////////////
+
+    /** @hide */
+    public AudioService(Context context) {
+        this(context, AudioSystemAdapter.getDefaultAdapter(),
+                SystemServerAdapter.getDefaultAdapter(context));
+    }
+
+    public AudioService(Context context, AudioSystemAdapter audioSystem,
+            SystemServerAdapter systemServer) {
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+        mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
+
+        mAudioSystem = audioSystem;
+        mSystemServer = systemServer;
+
+        mPlatformType = AudioSystem.getPlatformType(context);
+
+        mIsSingleVolume = AudioSystem.isSingleVolume(context);
+
+        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+
+        PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+        mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent");
+
+        mSfxHelper = new SoundEffectsHelper(mContext);
+
+        mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+        mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator();
+
+        // Initialize volume
+        // Priority 1 - Android Property
+        // Priority 2 - Audio Policy Service
+        // Priority 3 - Default Value
+        if (AudioProductStrategy.getAudioProductStrategies().size() > 0) {
+            int numStreamTypes = AudioSystem.getNumStreamTypes();
+
+            for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+                AudioAttributes attr =
+                        AudioProductStrategy.getAudioAttributesForStrategyWithLegacyStreamType(
+                                streamType);
+                int maxVolume = AudioSystem.getMaxVolumeIndexForAttributes(attr);
+                if (maxVolume != -1) {
+                    MAX_STREAM_VOLUME[streamType] = maxVolume;
+                }
+                int minVolume = AudioSystem.getMinVolumeIndexForAttributes(attr);
+                if (minVolume != -1) {
+                    MIN_STREAM_VOLUME[streamType] = minVolume;
+                }
+            }
+        }
+
+        int maxCallVolume = SystemProperties.getInt("ro.config.vc_call_vol_steps", -1);
+        if (maxCallVolume != -1) {
+            MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] = maxCallVolume;
+        }
+
+        int defaultCallVolume = SystemProperties.getInt("ro.config.vc_call_vol_default", -1);
+        if (defaultCallVolume != -1 &&
+                defaultCallVolume <= MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] &&
+                defaultCallVolume >= MIN_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL]) {
+            AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] = defaultCallVolume;
+        } else {
+            AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] =
+                    (MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] * 3) / 4;
+        }
+
+        int maxMusicVolume = SystemProperties.getInt("ro.config.media_vol_steps", -1);
+        if (maxMusicVolume != -1) {
+            MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] = maxMusicVolume;
+        }
+
+        int defaultMusicVolume = SystemProperties.getInt("ro.config.media_vol_default", -1);
+        if (defaultMusicVolume != -1 &&
+                defaultMusicVolume <= MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] &&
+                defaultMusicVolume >= MIN_STREAM_VOLUME[AudioSystem.STREAM_MUSIC]) {
+            AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] = defaultMusicVolume;
+        } else {
+            if (isPlatformTelevision()) {
+                AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] =
+                        MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] / 4;
+            } else {
+                AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] =
+                        MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] / 3;
+            }
+        }
+
+        int maxAlarmVolume = SystemProperties.getInt("ro.config.alarm_vol_steps", -1);
+        if (maxAlarmVolume != -1) {
+            MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM] = maxAlarmVolume;
+        }
+
+        int defaultAlarmVolume = SystemProperties.getInt("ro.config.alarm_vol_default", -1);
+        if (defaultAlarmVolume != -1 &&
+                defaultAlarmVolume <= MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM]) {
+            AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_ALARM] = defaultAlarmVolume;
+        } else {
+            // Default is 6 out of 7 (default maximum), so scale accordingly.
+            AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_ALARM] =
+                        6 * MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM] / 7;
+        }
+
+        int maxSystemVolume = SystemProperties.getInt("ro.config.system_vol_steps", -1);
+        if (maxSystemVolume != -1) {
+            MAX_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM] = maxSystemVolume;
+        }
+
+        int defaultSystemVolume = SystemProperties.getInt("ro.config.system_vol_default", -1);
+        if (defaultSystemVolume != -1 &&
+                defaultSystemVolume <= MAX_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM]) {
+            AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM] = defaultSystemVolume;
+        } else {
+            // Default is to use maximum.
+            AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM] =
+                        MAX_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM];
+        }
+
+        createAudioSystemThread();
+
+        AudioSystem.setErrorCallback(mAudioSystemCallback);
+
+        updateAudioHalPids();
+
+        boolean cameraSoundForced = readCameraSoundForced();
+        mCameraSoundForced = new Boolean(cameraSoundForced);
+        sendMsg(mAudioHandler,
+                MSG_SET_FORCE_USE,
+                SENDMSG_QUEUE,
+                AudioSystem.FOR_SYSTEM,
+                cameraSoundForced ?
+                        AudioSystem.FORCE_SYSTEM_ENFORCED : AudioSystem.FORCE_NONE,
+                new String("AudioService ctor"),
+                0);
+
+        mSafeMediaVolumeState = Settings.Global.getInt(mContentResolver,
+                                            Settings.Global.AUDIO_SAFE_VOLUME_STATE,
+                                            SAFE_MEDIA_VOLUME_NOT_CONFIGURED);
+        // The default safe volume index read here will be replaced by the actual value when
+        // the mcc is read by onConfigureSafeVolume()
+        mSafeMediaVolumeIndex = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_safe_media_volume_index) * 10;
+
+        mUseFixedVolume = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_useFixedVolume);
+
+        mDeviceBroker = new AudioDeviceBroker(mContext, this);
+
+        mRecordMonitor = new RecordingActivityMonitor(mContext);
+
+        // must be called before readPersistedSettings() which needs a valid mStreamVolumeAlias[]
+        // array initialized by updateStreamVolumeAlias()
+        updateStreamVolumeAlias(false /*updateVolumes*/, TAG);
+        readPersistedSettings();
+        readUserRestrictions();
+        mSettingsObserver = new SettingsObserver();
+        createStreamStates();
+
+        // must be called after createStreamStates() as it uses MUSIC volume as default if no
+        // persistent data
+        initVolumeGroupStates();
+
+        // mSafeUsbMediaVolumeIndex must be initialized after createStreamStates() because it
+        // relies on audio policy having correct ranges for volume indexes.
+        mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
+
+        mPlaybackMonitor =
+                new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM]);
+
+        mMediaFocusControl = new MediaFocusControl(mContext, mPlaybackMonitor);
+
+        readAndSetLowRamDevice();
+
+        mIsCallScreeningModeSupported = AudioSystem.isCallScreeningModeSupported();
+
+        // Call setRingerModeInt() to apply correct mute
+        // state on streams affected by ringer mode.
+        mRingerAndZenModeMutedStreams = 0;
+        setRingerModeInt(getRingerModeInternal(), false);
+
+        // Register for device connection intent broadcasts.
+        IntentFilter intentFilter =
+                new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
+        intentFilter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
+        intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
+        intentFilter.addAction(Intent.ACTION_SCREEN_ON);
+        intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+        intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
+        intentFilter.addAction(Intent.ACTION_USER_BACKGROUND);
+        intentFilter.addAction(Intent.ACTION_USER_FOREGROUND);
+        intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+        intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+        intentFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+
+        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        mMonitorRotation = SystemProperties.getBoolean("ro.audio.monitorRotation", false);
+        if (mMonitorRotation) {
+            RotationHelper.init(mContext, mAudioHandler);
+        }
+
+        intentFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
+        intentFilter.addAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
+
+        context.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null);
+
+        if (mSystemServer.isPrivileged()) {
+            LocalServices.addService(AudioManagerInternal.class, new AudioServiceInternal());
+
+            mUserManagerInternal.addUserRestrictionsListener(mUserRestrictionsListener);
+
+            mRecordMonitor.initMonitor();
+        }
+
+        final float[] preScale = new float[3];
+        preScale[0] = mContext.getResources().getFraction(
+                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index1,
+                1, 1);
+        preScale[1] = mContext.getResources().getFraction(
+                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index2,
+                1, 1);
+        preScale[2] = mContext.getResources().getFraction(
+                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index3,
+                1, 1);
+        for (int i = 0; i < preScale.length; i++) {
+            if (0.0f <= preScale[i] && preScale[i] <= 1.0f) {
+                mPrescaleAbsoluteVolume[i] = preScale[i];
+            }
+        }
+    }
+
+    public void systemReady() {
+        sendMsg(mAudioHandler, MSG_SYSTEM_READY, SENDMSG_QUEUE,
+                0, 0, null, 0);
+        if (false) {
+            // This is turned off for now, because it is racy and thus causes apps to break.
+            // Currently banning a uid means that if an app tries to start playing an audio
+            // stream, that will be preventing, and unbanning it will not allow that stream
+            // to resume.  However these changes in uid state are racy with what the app is doing,
+            // so that after taking a process out of the cached state we can't guarantee that
+            // we will unban the uid before the app actually tries to start playing audio.
+            // (To do that, the activity manager would need to wait until it knows for sure
+            // that the ban has been removed, before telling the app to do whatever it is
+            // supposed to do that caused it to go out of the cached state.)
+            try {
+                ActivityManager.getService().registerUidObserver(mUidObserver,
+                        ActivityManager.UID_OBSERVER_CACHED | ActivityManager.UID_OBSERVER_GONE,
+                        ActivityManager.PROCESS_STATE_UNKNOWN, null);
+            } catch (RemoteException e) {
+                // ignored; both services live in system_server
+            }
+        }
+    }
+
+    public void onSystemReady() {
+        mSystemReady = true;
+        scheduleLoadSoundEffects();
+
+        mDeviceBroker.onSystemReady();
+
+        if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_HDMI_CEC)) {
+            synchronized (mHdmiClientLock) {
+                mHdmiCecSink = false;
+                mHdmiManager = mContext.getSystemService(HdmiControlManager.class);
+                if (mHdmiManager != null) {
+                    mHdmiManager.addHdmiControlStatusChangeListener(
+                            mHdmiControlStatusChangeListenerCallback);
+                }
+                mHdmiTvClient = mHdmiManager.getTvClient();
+                if (mHdmiTvClient != null) {
+                    mFixedVolumeDevices.removeAll(
+                            AudioSystem.DEVICE_ALL_HDMI_SYSTEM_AUDIO_AND_SPEAKER_SET);
+                }
+                mHdmiPlaybackClient = mHdmiManager.getPlaybackClient();
+                if (mHdmiPlaybackClient != null) {
+                    // not a television: HDMI output will be always at max
+                    mFixedVolumeDevices.remove(AudioSystem.DEVICE_OUT_HDMI);
+                    mFullVolumeDevices.add(AudioSystem.DEVICE_OUT_HDMI);
+                }
+                mHdmiAudioSystemClient = mHdmiManager.getAudioSystemClient();
+            }
+        }
+
+        mNm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        sendMsg(mAudioHandler,
+                MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED,
+                SENDMSG_REPLACE,
+                0,
+                0,
+                TAG,
+                SystemProperties.getBoolean("audio.safemedia.bypass", false) ?
+                        0 : SAFE_VOLUME_CONFIGURE_TIMEOUT_MS);
+
+        initA11yMonitoring();
+
+        mRoleObserver = new RoleObserver();
+        mRoleObserver.register();
+
+        onIndicateSystemReady();
+
+        mMicMuteFromSystemCached = mAudioSystem.isMicrophoneMuted();
+        setMicMuteFromSwitchInput();
+    }
+
+    RoleObserver mRoleObserver;
+
+    class RoleObserver implements OnRoleHoldersChangedListener {
+        private RoleManager mRm;
+        private final Executor mExecutor;
+
+        RoleObserver() {
+            mExecutor = mContext.getMainExecutor();
+        }
+
+        public void register() {
+            mRm = (RoleManager) mContext.getSystemService(Context.ROLE_SERVICE);
+            if (mRm != null) {
+                mRm.addOnRoleHoldersChangedListenerAsUser(mExecutor, this, UserHandle.ALL);
+                updateAssistantUId(true);
+            }
+        }
+
+        @Override
+        public void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user) {
+            if (RoleManager.ROLE_ASSISTANT.equals(roleName)) {
+                updateAssistantUId(false);
+            }
+        }
+
+        public String getAssistantRoleHolder() {
+            String assitantPackage = "";
+            if (mRm != null) {
+                List<String> assistants = mRm.getRoleHolders(RoleManager.ROLE_ASSISTANT);
+                assitantPackage = assistants.size() == 0 ? "" : assistants.get(0);
+            }
+            return assitantPackage;
+        }
+    }
+
+    void onIndicateSystemReady() {
+        if (AudioSystem.systemReady() == AudioSystem.SUCCESS) {
+            return;
+        }
+        sendMsg(mAudioHandler,
+                MSG_INDICATE_SYSTEM_READY,
+                SENDMSG_REPLACE,
+                0,
+                0,
+                null,
+                INDICATE_SYSTEM_READY_RETRY_DELAY_MS);
+    }
+
+    public void onAudioServerDied() {
+        if (!mSystemReady ||
+                (AudioSystem.checkAudioFlinger() != AudioSystem.AUDIO_STATUS_OK)) {
+            Log.e(TAG, "Audioserver died.");
+            sendMsg(mAudioHandler, MSG_AUDIO_SERVER_DIED, SENDMSG_NOOP, 0, 0,
+                    null, 500);
+            return;
+        }
+        Log.e(TAG, "Audioserver started.");
+
+        updateAudioHalPids();
+
+        // indicate to audio HAL that we start the reconfiguration phase after a media
+        // server crash
+        // Note that we only execute this when the media server
+        // process restarts after a crash, not the first time it is started.
+        AudioSystem.setParameters("restarting=true");
+
+        readAndSetLowRamDevice();
+
+        mIsCallScreeningModeSupported = AudioSystem.isCallScreeningModeSupported();
+
+        // Restore device connection states, BT state
+        mDeviceBroker.onAudioServerDied();
+
+        // Restore call state
+        synchronized (mDeviceBroker.mSetModeLock) {
+            if (AudioSystem.setPhoneState(mMode, getModeOwnerUid())
+                    ==  AudioSystem.AUDIO_STATUS_OK) {
+                mModeLogger.log(new AudioEventLogger.StringEvent(
+                        "onAudioServerDied causes setPhoneState(" + AudioSystem.modeToString(mMode)
+                        + ", uid=" + getModeOwnerUid() + ")"));
+            }
+        }
+        final int forSys;
+        synchronized (mSettingsLock) {
+            forSys = mCameraSoundForced ?
+                    AudioSystem.FORCE_SYSTEM_ENFORCED : AudioSystem.FORCE_NONE;
+        }
+
+        mDeviceBroker.setForceUse_Async(AudioSystem.FOR_SYSTEM, forSys, "onAudioServerDied");
+
+        // Restore stream volumes
+        int numStreamTypes = AudioSystem.getNumStreamTypes();
+        for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+            VolumeStreamState streamState = mStreamStates[streamType];
+            AudioSystem.initStreamVolume(
+                streamType, streamState.mIndexMin / 10, streamState.mIndexMax / 10);
+
+            streamState.applyAllVolumes();
+        }
+
+        // Restore audio volume groups
+        restoreVolumeGroups();
+
+        // Restore mono mode
+        updateMasterMono(mContentResolver);
+
+        // Restore audio balance
+        updateMasterBalance(mContentResolver);
+
+        // Restore ringer mode
+        setRingerModeInt(getRingerModeInternal(), false);
+
+        // Reset device rotation (if monitored for this device)
+        if (mMonitorRotation) {
+            RotationHelper.updateOrientation();
+        }
+
+        synchronized (mSettingsLock) {
+            final int forDock = mDockAudioMediaEnabled ?
+                    AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE;
+            mDeviceBroker.setForceUse_Async(AudioSystem.FOR_DOCK, forDock, "onAudioServerDied");
+            sendEncodedSurroundMode(mContentResolver, "onAudioServerDied");
+            sendEnabledSurroundFormats(mContentResolver, true);
+            updateAssistantUId(true);
+            updateCurrentImeUid(true);
+            AudioSystem.setRttEnabled(mRttEnabled);
+        }
+        synchronized (mAccessibilityServiceUidsLock) {
+            AudioSystem.setA11yServicesUids(mAccessibilityServiceUids);
+        }
+        synchronized (mHdmiClientLock) {
+            if (mHdmiManager != null && mHdmiTvClient != null) {
+                setHdmiSystemAudioSupported(mHdmiSystemAudioSupported);
+            }
+        }
+
+        synchronized (mSupportedSystemUsagesLock) {
+            AudioSystem.setSupportedSystemUsages(mSupportedSystemUsages);
+        }
+
+        synchronized (mAudioPolicies) {
+            for (AudioPolicyProxy policy : mAudioPolicies.values()) {
+                final int status = policy.connectMixes();
+                if (status != AudioSystem.SUCCESS) {
+                    // note that PERMISSION_DENIED may also indicate trouble getting to APService
+                    Log.e(TAG, "onAudioServerDied: error "
+                            + AudioSystem.audioSystemErrorToString(status)
+                            + " when connecting mixes for policy " + policy.toLogFriendlyString());
+                    policy.release();
+                } else {
+                    final int deviceAffinitiesStatus = policy.setupDeviceAffinities();
+                    if (deviceAffinitiesStatus != AudioSystem.SUCCESS) {
+                        Log.e(TAG, "onAudioServerDied: error "
+                                + AudioSystem.audioSystemErrorToString(deviceAffinitiesStatus)
+                                + " when connecting device affinities for policy "
+                                + policy.toLogFriendlyString());
+                        policy.release();
+                    }
+                }
+            }
+        }
+
+        // Restore capture policies
+        synchronized (mPlaybackMonitor) {
+            HashMap<Integer, Integer> allowedCapturePolicies =
+                    mPlaybackMonitor.getAllAllowedCapturePolicies();
+            for (HashMap.Entry<Integer, Integer> entry : allowedCapturePolicies.entrySet()) {
+                int result = AudioSystem.setAllowedCapturePolicy(
+                        entry.getKey(),
+                        AudioAttributes.capturePolicyToFlags(entry.getValue(), 0x0));
+                if (result != AudioSystem.AUDIO_STATUS_OK) {
+                    Log.e(TAG, "Failed to restore capture policy, uid: "
+                            + entry.getKey() + ", capture policy: " + entry.getValue()
+                            + ", result: " + result);
+                    // When restoring capture policy failed, set the capture policy as
+                    // ALLOW_CAPTURE_BY_ALL, which will result in removing the cached
+                    // capture policy in PlaybackActivityMonitor.
+                    mPlaybackMonitor.setAllowedCapturePolicy(
+                            entry.getKey(), AudioAttributes.ALLOW_CAPTURE_BY_ALL);
+                }
+            }
+        }
+
+        onIndicateSystemReady();
+        // indicate the end of reconfiguration phase to audio HAL
+        AudioSystem.setParameters("restarting=false");
+
+        sendMsg(mAudioHandler, MSG_DISPATCH_AUDIO_SERVER_STATE,
+                SENDMSG_QUEUE, 1, 0, null, 0);
+
+        setMicrophoneMuteNoCallerCheck(getCurrentUserId()); // will also update the mic mute cache
+        setMicMuteFromSwitchInput();
+    }
+
+    private void onDispatchAudioServerStateChange(boolean state) {
+        synchronized (mAudioServerStateListeners) {
+            for (AsdProxy asdp : mAudioServerStateListeners.values()) {
+                try {
+                    asdp.callback().dispatchAudioServerStateChange(state);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Could not call dispatchAudioServerStateChange()", e);
+                }
+            }
+        }
+    }
+
+    private void createAudioSystemThread() {
+        mAudioSystemThread = new AudioSystemThread();
+        mAudioSystemThread.start();
+        waitForAudioHandlerCreation();
+    }
+
+    /** Waits for the volume handler to be created by the other thread. */
+    private void waitForAudioHandlerCreation() {
+        synchronized(this) {
+            while (mAudioHandler == null) {
+                try {
+                    // Wait for mAudioHandler to be set by the other thread
+                    wait();
+                } catch (InterruptedException e) {
+                    Log.e(TAG, "Interrupted while waiting on volume handler.");
+                }
+            }
+        }
+    }
+
+    /**
+     * @see AudioManager#setSupportedSystemUsages(int[])
+     */
+    public void setSupportedSystemUsages(@NonNull @AttributeSystemUsage int[] systemUsages) {
+        enforceModifyAudioRoutingPermission();
+        verifySystemUsages(systemUsages);
+
+        synchronized (mSupportedSystemUsagesLock) {
+            AudioSystem.setSupportedSystemUsages(systemUsages);
+            mSupportedSystemUsages = systemUsages;
+        }
+    }
+
+    /**
+     * @see AudioManager#getSupportedSystemUsages()
+     */
+    public @NonNull @AttributeSystemUsage int[] getSupportedSystemUsages() {
+        enforceModifyAudioRoutingPermission();
+        synchronized (mSupportedSystemUsagesLock) {
+            return Arrays.copyOf(mSupportedSystemUsages, mSupportedSystemUsages.length);
+        }
+    }
+
+    private void verifySystemUsages(@NonNull int[] systemUsages) {
+        for (int i = 0; i < systemUsages.length; i++) {
+            if (!AudioAttributes.isSystemUsage(systemUsages[i])) {
+                throw new IllegalArgumentException("Non-system usage provided: " + systemUsages[i]);
+            }
+        }
+    }
+
+    /**
+     * @return the {@link android.media.audiopolicy.AudioProductStrategy} discovered from the
+     * platform configuration file.
+     */
+    @NonNull
+    public List<AudioProductStrategy> getAudioProductStrategies() {
+        return AudioProductStrategy.getAudioProductStrategies();
+    }
+
+    /**
+     * @return the List of {@link android.media.audiopolicy.AudioVolumeGroup} discovered from the
+     * platform configuration file.
+     */
+    @NonNull
+    public List<AudioVolumeGroup> getAudioVolumeGroups() {
+        return AudioVolumeGroup.getAudioVolumeGroups();
+    }
+
+    private void checkAllAliasStreamVolumes() {
+        synchronized (mSettingsLock) {
+            synchronized (VolumeStreamState.class) {
+                int numStreamTypes = AudioSystem.getNumStreamTypes();
+                for (int streamType = 0; streamType < numStreamTypes; streamType++) {
+                    mStreamStates[streamType]
+                            .setAllIndexes(mStreamStates[mStreamVolumeAlias[streamType]], TAG);
+                    // apply stream volume
+                    if (!mStreamStates[streamType].mIsMuted) {
+                        mStreamStates[streamType].applyAllVolumes();
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Called from AudioDeviceBroker when DEVICE_OUT_HDMI is connected or disconnected.
+     */
+    /*package*/ void postCheckVolumeCecOnHdmiConnection(
+            @AudioService.ConnectionState  int state, String caller) {
+        sendMsg(mAudioHandler, MSG_HDMI_VOLUME_CHECK, SENDMSG_REPLACE,
+                state /*arg1*/, 0 /*arg2 ignored*/, caller /*obj*/, 0 /*delay*/);
+    }
+
+    private void onCheckVolumeCecOnHdmiConnection(
+            @AudioService.ConnectionState int state, String caller) {
+        if (state == AudioService.CONNECTION_STATE_CONNECTED) {
+            // DEVICE_OUT_HDMI is now connected
+            if (mSafeMediaVolumeDevices.contains(AudioSystem.DEVICE_OUT_HDMI)) {
+                sendMsg(mAudioHandler,
+                        MSG_CHECK_MUSIC_ACTIVE,
+                        SENDMSG_REPLACE,
+                        0,
+                        0,
+                        caller,
+                        MUSIC_ACTIVE_POLL_PERIOD_MS);
+            }
+
+            if (isPlatformTelevision()) {
+                checkAddAllFixedVolumeDevices(AudioSystem.DEVICE_OUT_HDMI, caller);
+                synchronized (mHdmiClientLock) {
+                    if (mHdmiManager != null && mHdmiPlaybackClient != null) {
+                        updateHdmiCecSinkLocked(mHdmiCecSink | false);
+                    }
+                }
+            }
+            sendEnabledSurroundFormats(mContentResolver, true);
+        } else {
+            // DEVICE_OUT_HDMI disconnected
+            if (isPlatformTelevision()) {
+                synchronized (mHdmiClientLock) {
+                    if (mHdmiManager != null) {
+                        updateHdmiCecSinkLocked(mHdmiCecSink | false);
+                    }
+                }
+            }
+        }
+    }
+
+    private void checkAddAllFixedVolumeDevices(int device, String caller) {
+        final int numStreamTypes = AudioSystem.getNumStreamTypes();
+        for (int streamType = 0; streamType < numStreamTypes; streamType++) {
+            if (!mStreamStates[streamType].hasIndexForDevice(device)) {
+                // set the default value, if device is affected by a full/fix/abs volume rule, it
+                // will taken into account in checkFixedVolumeDevices()
+                mStreamStates[streamType].setIndex(
+                        mStreamStates[mStreamVolumeAlias[streamType]]
+                                .getIndex(AudioSystem.DEVICE_OUT_DEFAULT),
+                        device, caller);
+            }
+            mStreamStates[streamType].checkFixedVolumeDevices();
+        }
+    }
+
+    private void checkAllFixedVolumeDevices()
+    {
+        int numStreamTypes = AudioSystem.getNumStreamTypes();
+        for (int streamType = 0; streamType < numStreamTypes; streamType++) {
+            mStreamStates[streamType].checkFixedVolumeDevices();
+        }
+    }
+
+    private void checkAllFixedVolumeDevices(int streamType) {
+        mStreamStates[streamType].checkFixedVolumeDevices();
+    }
+
+    private void checkMuteAffectedStreams() {
+        // any stream with a min level > 0 is not muteable by definition
+        // STREAM_VOICE_CALL and STREAM_BLUETOOTH_SCO can be muted by applications
+        // that has the the MODIFY_PHONE_STATE permission.
+        for (int i = 0; i < mStreamStates.length; i++) {
+            final VolumeStreamState vss = mStreamStates[i];
+            if (vss.mIndexMin > 0 &&
+                (vss.mStreamType != AudioSystem.STREAM_VOICE_CALL &&
+                vss.mStreamType != AudioSystem.STREAM_BLUETOOTH_SCO)) {
+                mMuteAffectedStreams &= ~(1 << vss.mStreamType);
+            }
+        }
+    }
+
+    private void createStreamStates() {
+        int numStreamTypes = AudioSystem.getNumStreamTypes();
+        VolumeStreamState[] streams = mStreamStates = new VolumeStreamState[numStreamTypes];
+
+        for (int i = 0; i < numStreamTypes; i++) {
+            streams[i] =
+                    new VolumeStreamState(System.VOLUME_SETTINGS_INT[mStreamVolumeAlias[i]], i);
+        }
+
+        checkAllFixedVolumeDevices();
+        checkAllAliasStreamVolumes();
+        checkMuteAffectedStreams();
+        updateDefaultVolumes();
+    }
+
+    // Update default indexes from aliased streams. Must be called after mStreamStates is created
+    private void updateDefaultVolumes() {
+        for (int stream = 0; stream < mStreamStates.length; stream++) {
+            if (stream != mStreamVolumeAlias[stream]) {
+                AudioSystem.DEFAULT_STREAM_VOLUME[stream] = (rescaleIndex(
+                        AudioSystem.DEFAULT_STREAM_VOLUME[mStreamVolumeAlias[stream]] * 10,
+                        mStreamVolumeAlias[stream],
+                        stream) + 5) / 10;
+            }
+        }
+    }
+
+    private void dumpStreamStates(PrintWriter pw) {
+        pw.println("\nStream volumes (device: index)");
+        int numStreamTypes = AudioSystem.getNumStreamTypes();
+        for (int i = 0; i < numStreamTypes; i++) {
+            pw.println("- " + AudioSystem.STREAM_NAMES[i] + ":");
+            mStreamStates[i].dump(pw);
+            pw.println("");
+        }
+        pw.print("\n- mute affected streams = 0x");
+        pw.println(Integer.toHexString(mMuteAffectedStreams));
+    }
+
+    private void updateStreamVolumeAlias(boolean updateVolumes, String caller) {
+        int dtmfStreamAlias;
+        final int a11yStreamAlias = sIndependentA11yVolume ?
+                AudioSystem.STREAM_ACCESSIBILITY : AudioSystem.STREAM_MUSIC;
+        final int assistantStreamAlias = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_useAssistantVolume) ?
+                AudioSystem.STREAM_ASSISTANT : AudioSystem.STREAM_MUSIC;
+
+        if (mIsSingleVolume) {
+            mStreamVolumeAlias = STREAM_VOLUME_ALIAS_TELEVISION;
+            dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
+        } else {
+            switch (mPlatformType) {
+                case AudioSystem.PLATFORM_VOICE:
+                    mStreamVolumeAlias = STREAM_VOLUME_ALIAS_VOICE;
+                    dtmfStreamAlias = AudioSystem.STREAM_RING;
+                    break;
+                default:
+                    mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT;
+                    dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
+            }
+        }
+
+        if (mIsSingleVolume) {
+            mRingerModeAffectedStreams = 0;
+        } else {
+            if (isInCommunication()) {
+                dtmfStreamAlias = AudioSystem.STREAM_VOICE_CALL;
+                mRingerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_DTMF);
+            } else {
+                mRingerModeAffectedStreams |= (1 << AudioSystem.STREAM_DTMF);
+            }
+        }
+
+        mStreamVolumeAlias[AudioSystem.STREAM_DTMF] = dtmfStreamAlias;
+        mStreamVolumeAlias[AudioSystem.STREAM_ACCESSIBILITY] = a11yStreamAlias;
+        mStreamVolumeAlias[AudioSystem.STREAM_ASSISTANT] = assistantStreamAlias;
+
+        if (updateVolumes && mStreamStates != null) {
+            updateDefaultVolumes();
+
+            synchronized (mSettingsLock) {
+                synchronized (VolumeStreamState.class) {
+                    mStreamStates[AudioSystem.STREAM_DTMF]
+                            .setAllIndexes(mStreamStates[dtmfStreamAlias], caller);
+                    mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].mVolumeIndexSettingName =
+                            System.VOLUME_SETTINGS_INT[a11yStreamAlias];
+                    mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].setAllIndexes(
+                            mStreamStates[a11yStreamAlias], caller);
+                }
+            }
+            if (sIndependentA11yVolume) {
+                // restore the a11y values from the settings
+                mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].readSettings();
+            }
+
+            // apply stream mute states according to new value of mRingerModeAffectedStreams
+            setRingerModeInt(getRingerModeInternal(), false);
+            sendMsg(mAudioHandler,
+                    MSG_SET_ALL_VOLUMES,
+                    SENDMSG_QUEUE,
+                    0,
+                    0,
+                    mStreamStates[AudioSystem.STREAM_DTMF], 0);
+            sendMsg(mAudioHandler,
+                    MSG_SET_ALL_VOLUMES,
+                    SENDMSG_QUEUE,
+                    0,
+                    0,
+                    mStreamStates[AudioSystem.STREAM_ACCESSIBILITY], 0);
+        }
+    }
+
+    private void readDockAudioSettings(ContentResolver cr)
+    {
+        mDockAudioMediaEnabled = Settings.Global.getInt(
+                                        cr, Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 0) == 1;
+
+        sendMsg(mAudioHandler,
+                MSG_SET_FORCE_USE,
+                SENDMSG_QUEUE,
+                AudioSystem.FOR_DOCK,
+                mDockAudioMediaEnabled ?
+                        AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE,
+                new String("readDockAudioSettings"),
+                0);
+    }
+
+
+    private void updateMasterMono(ContentResolver cr)
+    {
+        final boolean masterMono = System.getIntForUser(
+                cr, System.MASTER_MONO, 0 /* default */, UserHandle.USER_CURRENT) == 1;
+        if (DEBUG_VOL) {
+            Log.d(TAG, String.format("Master mono %b", masterMono));
+        }
+        AudioSystem.setMasterMono(masterMono);
+    }
+
+    private void updateMasterBalance(ContentResolver cr) {
+        final float masterBalance = System.getFloatForUser(
+                cr, System.MASTER_BALANCE, 0.f /* default */, UserHandle.USER_CURRENT);
+        if (DEBUG_VOL) {
+            Log.d(TAG, String.format("Master balance %f", masterBalance));
+        }
+        if (AudioSystem.setMasterBalance(masterBalance) != 0) {
+            Log.e(TAG, String.format("setMasterBalance failed for %f", masterBalance));
+        }
+    }
+
+    private void sendEncodedSurroundMode(ContentResolver cr, String eventSource)
+    {
+        final int encodedSurroundMode = Settings.Global.getInt(
+                cr, Settings.Global.ENCODED_SURROUND_OUTPUT,
+                Settings.Global.ENCODED_SURROUND_OUTPUT_AUTO);
+        sendEncodedSurroundMode(encodedSurroundMode, eventSource);
+    }
+
+    private void sendEncodedSurroundMode(int encodedSurroundMode, String eventSource)
+    {
+        // initialize to guaranteed bad value
+        int forceSetting = AudioSystem.NUM_FORCE_CONFIG;
+        switch (encodedSurroundMode) {
+            case Settings.Global.ENCODED_SURROUND_OUTPUT_AUTO:
+                forceSetting = AudioSystem.FORCE_NONE;
+                break;
+            case Settings.Global.ENCODED_SURROUND_OUTPUT_NEVER:
+                forceSetting = AudioSystem.FORCE_ENCODED_SURROUND_NEVER;
+                break;
+            case Settings.Global.ENCODED_SURROUND_OUTPUT_ALWAYS:
+                forceSetting = AudioSystem.FORCE_ENCODED_SURROUND_ALWAYS;
+                break;
+            case Settings.Global.ENCODED_SURROUND_OUTPUT_MANUAL:
+                forceSetting = AudioSystem.FORCE_ENCODED_SURROUND_MANUAL;
+                break;
+            default:
+                Log.e(TAG, "updateSurroundSoundSettings: illegal value "
+                        + encodedSurroundMode);
+                break;
+        }
+        if (forceSetting != AudioSystem.NUM_FORCE_CONFIG) {
+            mDeviceBroker.setForceUse_Async(AudioSystem.FOR_ENCODED_SURROUND, forceSetting,
+                    eventSource);
+        }
+    }
+
+    private void sendEnabledSurroundFormats(ContentResolver cr, boolean forceUpdate) {
+        if (mEncodedSurroundMode != Settings.Global.ENCODED_SURROUND_OUTPUT_MANUAL) {
+            // Manually enable surround formats only when the setting is in manual mode.
+            return;
+        }
+        String enabledSurroundFormats = Settings.Global.getString(
+                cr, Settings.Global.ENCODED_SURROUND_OUTPUT_ENABLED_FORMATS);
+        if (enabledSurroundFormats == null) {
+            // Never allow enabledSurroundFormats as a null, which could happen when
+            // ENCODED_SURROUND_OUTPUT_ENABLED_FORMATS is not appear in settings DB.
+            enabledSurroundFormats = "";
+        }
+        if (!forceUpdate && TextUtils.equals(enabledSurroundFormats, mEnabledSurroundFormats)) {
+            // Update enabled surround formats to AudioPolicyManager only when forceUpdate
+            // is true or enabled surround formats changed.
+            return;
+        }
+
+        mEnabledSurroundFormats = enabledSurroundFormats;
+        String[] surroundFormats = TextUtils.split(enabledSurroundFormats, ",");
+        ArrayList<Integer> formats = new ArrayList<>();
+        for (String format : surroundFormats) {
+            try {
+                int audioFormat = Integer.valueOf(format);
+                boolean isSurroundFormat = false;
+                for (int sf : AudioFormat.SURROUND_SOUND_ENCODING) {
+                    if (sf == audioFormat) {
+                        isSurroundFormat = true;
+                        break;
+                    }
+                }
+                if (isSurroundFormat && !formats.contains(audioFormat)) {
+                    formats.add(audioFormat);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Invalid enabled surround format:" + format);
+            }
+        }
+        // Set filtered surround formats to settings DB in case
+        // there are invalid surround formats in original settings.
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.ENCODED_SURROUND_OUTPUT_ENABLED_FORMATS,
+                TextUtils.join(",", formats));
+        sendMsg(mAudioHandler, MSG_ENABLE_SURROUND_FORMATS, SENDMSG_QUEUE, 0, 0, formats, 0);
+    }
+
+    private void onEnableSurroundFormats(ArrayList<Integer> enabledSurroundFormats) {
+        // Set surround format enabled accordingly.
+        for (int surroundFormat : AudioFormat.SURROUND_SOUND_ENCODING) {
+            boolean enabled = enabledSurroundFormats.contains(surroundFormat);
+            int ret = AudioSystem.setSurroundFormatEnabled(surroundFormat, enabled);
+            Log.i(TAG, "enable surround format:" + surroundFormat + " " + enabled + " " + ret);
+        }
+    }
+
+    @GuardedBy("mSettingsLock")
+    private void updateAssistantUId(boolean forceUpdate) {
+        int assistantUid = 0;
+
+        // Consider assistants in the following order of priority:
+        // 1) apk in assistant role
+        // 2) voice interaction service
+        // 3) assistant service
+
+        String packageName = "";
+        if (mRoleObserver != null) {
+            packageName = mRoleObserver.getAssistantRoleHolder();
+        }
+        if (TextUtils.isEmpty(packageName)) {
+            String assistantName = Settings.Secure.getStringForUser(
+                            mContentResolver,
+                            Settings.Secure.VOICE_INTERACTION_SERVICE, UserHandle.USER_CURRENT);
+            if (TextUtils.isEmpty(assistantName)) {
+                assistantName = Settings.Secure.getStringForUser(
+                        mContentResolver,
+                        Settings.Secure.ASSISTANT, UserHandle.USER_CURRENT);
+            }
+            if (!TextUtils.isEmpty(assistantName)) {
+                ComponentName componentName = ComponentName.unflattenFromString(assistantName);
+                if (componentName == null) {
+                    Slog.w(TAG, "Invalid service name for "
+                            + Settings.Secure.VOICE_INTERACTION_SERVICE + ": " + assistantName);
+                    return;
+                }
+                packageName = componentName.getPackageName();
+            }
+        }
+        if (!TextUtils.isEmpty(packageName)) {
+            PackageManager pm = mContext.getPackageManager();
+            ActivityManager am =
+                          (ActivityManager) mContext.getSystemService(mContext.ACTIVITY_SERVICE);
+
+            if (pm.checkPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD, packageName)
+                    == PackageManager.PERMISSION_GRANTED) {
+                try {
+                    assistantUid = pm.getPackageUidAsUser(packageName, am.getCurrentUser());
+                } catch (PackageManager.NameNotFoundException e) {
+                    Log.e(TAG,
+                            "updateAssistantUId() could not find UID for package: " + packageName);
+                }
+            }
+        }
+
+        if (assistantUid != mAssistantUid || forceUpdate) {
+            AudioSystem.setAssistantUid(assistantUid);
+            mAssistantUid = assistantUid;
+        }
+    }
+
+    @GuardedBy("mSettingsLock")
+    private void updateCurrentImeUid(boolean forceUpdate) {
+        String imeId = Settings.Secure.getStringForUser(
+                mContentResolver,
+                Settings.Secure.DEFAULT_INPUT_METHOD, UserHandle.USER_CURRENT);
+        if (TextUtils.isEmpty(imeId)) {
+            Log.e(TAG, "updateCurrentImeUid() could not find current IME");
+            return;
+        }
+        ComponentName componentName = ComponentName.unflattenFromString(imeId);
+        if (componentName == null) {
+            Log.e(TAG, "updateCurrentImeUid() got invalid service name for "
+                    + Settings.Secure.DEFAULT_INPUT_METHOD + ": " + imeId);
+            return;
+        }
+        String packageName = componentName.getPackageName();
+        int currentUserId = LocalServices.getService(ActivityManagerInternal.class)
+                .getCurrentUserId();
+        int currentImeUid = LocalServices.getService(PackageManagerInternal.class)
+                .getPackageUidInternal(packageName, 0 /* flags */, currentUserId);
+        if (currentImeUid < 0) {
+            Log.e(TAG, "updateCurrentImeUid() could not find UID for package: " + packageName);
+            return;
+        }
+
+        if (currentImeUid != mCurrentImeUid || forceUpdate) {
+            mAudioSystem.setCurrentImeUid(currentImeUid);
+            mCurrentImeUid = currentImeUid;
+        }
+    }
+
+    private void readPersistedSettings() {
+        if (!mSystemServer.isPrivileged()) {
+            return;
+        }
+        final ContentResolver cr = mContentResolver;
+
+        int ringerModeFromSettings =
+                Settings.Global.getInt(
+                        cr, Settings.Global.MODE_RINGER, AudioManager.RINGER_MODE_NORMAL);
+        int ringerMode = ringerModeFromSettings;
+        // sanity check in case the settings are restored from a device with incompatible
+        // ringer modes
+        if (!isValidRingerMode(ringerMode)) {
+            ringerMode = AudioManager.RINGER_MODE_NORMAL;
+        }
+        if ((ringerMode == AudioManager.RINGER_MODE_VIBRATE) && !mHasVibrator) {
+            ringerMode = AudioManager.RINGER_MODE_SILENT;
+        }
+        if (ringerMode != ringerModeFromSettings) {
+            Settings.Global.putInt(cr, Settings.Global.MODE_RINGER, ringerMode);
+        }
+        if (mUseFixedVolume || mIsSingleVolume) {
+            ringerMode = AudioManager.RINGER_MODE_NORMAL;
+        }
+        synchronized(mSettingsLock) {
+            mRingerMode = ringerMode;
+            if (mRingerModeExternal == -1) {
+                mRingerModeExternal = mRingerMode;
+            }
+
+            // System.VIBRATE_ON is not used any more but defaults for mVibrateSetting
+            // are still needed while setVibrateSetting() and getVibrateSetting() are being
+            // deprecated.
+            mVibrateSetting = AudioSystem.getValueForVibrateSetting(0,
+                                            AudioManager.VIBRATE_TYPE_NOTIFICATION,
+                                            mHasVibrator ? AudioManager.VIBRATE_SETTING_ONLY_SILENT
+                                                            : AudioManager.VIBRATE_SETTING_OFF);
+            mVibrateSetting = AudioSystem.getValueForVibrateSetting(mVibrateSetting,
+                                            AudioManager.VIBRATE_TYPE_RINGER,
+                                            mHasVibrator ? AudioManager.VIBRATE_SETTING_ONLY_SILENT
+                                                            : AudioManager.VIBRATE_SETTING_OFF);
+
+            updateRingerAndZenModeAffectedStreams();
+            readDockAudioSettings(cr);
+            sendEncodedSurroundMode(cr, "readPersistedSettings");
+            sendEnabledSurroundFormats(cr, true);
+            updateAssistantUId(true);
+            updateCurrentImeUid(true);
+            AudioSystem.setRttEnabled(mRttEnabled);
+        }
+
+        mMuteAffectedStreams = System.getIntForUser(cr,
+                System.MUTE_STREAMS_AFFECTED, AudioSystem.DEFAULT_MUTE_STREAMS_AFFECTED,
+                UserHandle.USER_CURRENT);
+
+        updateMasterMono(cr);
+
+        updateMasterBalance(cr);
+
+        // Each stream will read its own persisted settings
+
+        // Broadcast the sticky intents
+        broadcastRingerMode(AudioManager.RINGER_MODE_CHANGED_ACTION, mRingerModeExternal);
+        broadcastRingerMode(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, mRingerMode);
+
+        // Broadcast vibrate settings
+        broadcastVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER);
+        broadcastVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION);
+
+        // Load settings for the volume controller
+        mVolumeController.loadSettings(cr);
+    }
+
+    private void readUserRestrictions() {
+        if (!mSystemServer.isPrivileged()) {
+            return;
+        }
+        final int currentUser = getCurrentUserId();
+
+        // Check the current user restriction.
+        boolean masterMute =
+                mUserManagerInternal.getUserRestriction(currentUser,
+                        UserManager.DISALLOW_UNMUTE_DEVICE)
+                        || mUserManagerInternal.getUserRestriction(currentUser,
+                        UserManager.DISALLOW_ADJUST_VOLUME);
+        if (mUseFixedVolume) {
+            masterMute = false;
+            AudioSystem.setMasterVolume(1.0f);
+        }
+        if (DEBUG_VOL) {
+            Log.d(TAG, String.format("Master mute %s, user=%d", masterMute, currentUser));
+        }
+        setSystemAudioMute(masterMute);
+        AudioSystem.setMasterMute(masterMute);
+        broadcastMasterMuteStatus(masterMute);
+
+        mMicMuteFromRestrictions = mUserManagerInternal.getUserRestriction(
+                currentUser, UserManager.DISALLOW_UNMUTE_MICROPHONE);
+        if (DEBUG_VOL) {
+            Log.d(TAG, String.format("Mic mute %b, user=%d", mMicMuteFromRestrictions,
+                    currentUser));
+        }
+        setMicrophoneMuteNoCallerCheck(currentUser);
+    }
+
+    private int getIndexRange(int streamType) {
+        return (mStreamStates[streamType].getMaxIndex() - mStreamStates[streamType].getMinIndex());
+    }
+
+    private int rescaleIndex(int index, int srcStream, int dstStream) {
+        int srcRange = getIndexRange(srcStream);
+        int dstRange = getIndexRange(dstStream);
+        if (srcRange == 0) {
+            Log.e(TAG, "rescaleIndex : index range should not be zero");
+            return mStreamStates[dstStream].getMinIndex();
+        }
+
+        return mStreamStates[dstStream].getMinIndex()
+                + ((index - mStreamStates[srcStream].getMinIndex()) * dstRange + srcRange / 2)
+                / srcRange;
+    }
+
+    private int rescaleStep(int step, int srcStream, int dstStream) {
+        int srcRange = getIndexRange(srcStream);
+        int dstRange = getIndexRange(dstStream);
+        if (srcRange == 0) {
+            Log.e(TAG, "rescaleStep : index range should not be zero");
+            return 0;
+        }
+
+        return ((step * dstRange + srcRange / 2) / srcRange);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // IPC methods
+    ///////////////////////////////////////////////////////////////////////////
+    /** @see AudioManager#setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceInfo) */
+    public int setPreferredDeviceForStrategy(int strategy, AudioDeviceAttributes device) {
+        if (device == null) {
+            return AudioSystem.ERROR;
+        }
+        enforceModifyAudioRoutingPermission();
+        final String logString = String.format(
+                "setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s",
+                Binder.getCallingUid(), Binder.getCallingPid(), strategy, device.toString());
+        sDeviceLogger.log(new AudioEventLogger.StringEvent(logString).printLog(TAG));
+        if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) {
+            Log.e(TAG, "Unsupported input routing in " + logString);
+            return AudioSystem.ERROR;
+        }
+
+        final int status = mDeviceBroker.setPreferredDeviceForStrategySync(strategy, device);
+        if (status != AudioSystem.SUCCESS) {
+            Log.e(TAG, String.format("Error %d in %s)", status, logString));
+        }
+
+        return status;
+    }
+
+    /** @see AudioManager#removePreferredDeviceForStrategy(AudioProductStrategy) */
+    public int removePreferredDeviceForStrategy(int strategy) {
+        enforceModifyAudioRoutingPermission();
+        final String logString =
+                String.format("removePreferredDeviceForStrategy strat:%d", strategy);
+        sDeviceLogger.log(new AudioEventLogger.StringEvent(logString).printLog(TAG));
+
+        final int status = mDeviceBroker.removePreferredDeviceForStrategySync(strategy);
+        if (status != AudioSystem.SUCCESS) {
+            Log.e(TAG, String.format("Error %d in %s)", status, logString));
+        }
+        return status;
+    }
+
+    /** @see AudioManager#getPreferredDeviceForStrategy(AudioProductStrategy) */
+    public AudioDeviceAttributes getPreferredDeviceForStrategy(int strategy) {
+        enforceModifyAudioRoutingPermission();
+        AudioDeviceAttributes[] devices = new AudioDeviceAttributes[1];
+        final long identity = Binder.clearCallingIdentity();
+        final int status = AudioSystem.getPreferredDeviceForStrategy(strategy, devices);
+        Binder.restoreCallingIdentity(identity);
+        if (status != AudioSystem.SUCCESS) {
+            Log.e(TAG, String.format("Error %d in getPreferredDeviceForStrategy(%d)",
+                    status, strategy));
+            return null;
+        } else {
+            return devices[0];
+        }
+    }
+
+    /** @see AudioManager#addOnPreferredDeviceForStrategyChangedListener(Executor, AudioManager.OnPreferredDeviceForStrategyChangedListener) */
+    public void registerStrategyPreferredDeviceDispatcher(
+            @Nullable IStrategyPreferredDeviceDispatcher dispatcher) {
+        if (dispatcher == null) {
+            return;
+        }
+        enforceModifyAudioRoutingPermission();
+        mDeviceBroker.registerStrategyPreferredDeviceDispatcher(dispatcher);
+    }
+
+    /** @see AudioManager#removeOnPreferredDeviceForStrategyChangedListener(AudioManager.OnPreferredDeviceForStrategyChangedListener) */
+    public void unregisterStrategyPreferredDeviceDispatcher(
+            @Nullable IStrategyPreferredDeviceDispatcher dispatcher) {
+        if (dispatcher == null) {
+            return;
+        }
+        enforceModifyAudioRoutingPermission();
+        mDeviceBroker.unregisterStrategyPreferredDeviceDispatcher(dispatcher);
+    }
+
+    /** @see AudioManager#getDevicesForAttributes(AudioAttributes) */
+    public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributes(
+            @NonNull AudioAttributes attributes) {
+        Objects.requireNonNull(attributes);
+        enforceModifyAudioRoutingPermission();
+        return AudioSystem.getDevicesForAttributes(attributes);
+    }
+
+
+    /** @see AudioManager#adjustVolume(int, int) */
+    public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags,
+            String callingPackage, String caller) {
+        final IAudioPolicyCallback extVolCtlr;
+        synchronized (mExtVolumeControllerLock) {
+            extVolCtlr = mExtVolumeController;
+        }
+        new MediaMetrics.Item(mMetricsId + "adjustSuggestedStreamVolume")
+                .setUid(Binder.getCallingUid())
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackage)
+                .set(MediaMetrics.Property.CLIENT_NAME, caller)
+                .set(MediaMetrics.Property.DIRECTION, direction > 0
+                        ? MediaMetrics.Value.UP : MediaMetrics.Value.DOWN)
+                .set(MediaMetrics.Property.EXTERNAL, extVolCtlr != null
+                        ? MediaMetrics.Value.YES : MediaMetrics.Value.NO)
+                .set(MediaMetrics.Property.FLAGS, flags)
+                .record();
+        if (extVolCtlr != null) {
+            sendMsg(mAudioHandler, MSG_NOTIFY_VOL_EVENT, SENDMSG_QUEUE,
+                    direction, 0 /*ignored*/,
+                    extVolCtlr, 0 /*delay*/);
+        } else {
+            adjustSuggestedStreamVolume(direction, suggestedStreamType, flags, callingPackage,
+                    caller, Binder.getCallingUid());
+        }
+    }
+
+    private void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags,
+            String callingPackage, String caller, int uid) {
+        if (DEBUG_VOL) Log.d(TAG, "adjustSuggestedStreamVolume() stream=" + suggestedStreamType
+                + ", flags=" + flags + ", caller=" + caller
+                + ", volControlStream=" + mVolumeControlStream
+                + ", userSelect=" + mUserSelectedVolumeControlStream);
+        if (direction != AudioManager.ADJUST_SAME) {
+            sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_SUGG_VOL, suggestedStreamType,
+                    direction/*val1*/, flags/*val2*/, new StringBuilder(callingPackage)
+                    .append("/").append(caller).append(" uid:").append(uid).toString()));
+        }
+        final int streamType;
+        synchronized (mForceControlStreamLock) {
+            // Request lock in case mVolumeControlStream is changed by other thread.
+            if (mUserSelectedVolumeControlStream) { // implies mVolumeControlStream != -1
+                streamType = mVolumeControlStream;
+            } else {
+                final int maybeActiveStreamType = getActiveStreamType(suggestedStreamType);
+                final boolean activeForReal;
+                if (maybeActiveStreamType == AudioSystem.STREAM_RING
+                        || maybeActiveStreamType == AudioSystem.STREAM_NOTIFICATION) {
+                    activeForReal = wasStreamActiveRecently(maybeActiveStreamType, 0);
+                } else {
+                    activeForReal = AudioSystem.isStreamActive(maybeActiveStreamType, 0);
+                }
+                if (activeForReal || mVolumeControlStream == -1) {
+                    streamType = maybeActiveStreamType;
+                } else {
+                    streamType = mVolumeControlStream;
+                }
+            }
+        }
+
+        final boolean isMute = isMuteAdjust(direction);
+
+        ensureValidStreamType(streamType);
+        final int resolvedStream = mStreamVolumeAlias[streamType];
+
+        // Play sounds on STREAM_RING only.
+        if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 &&
+                resolvedStream != AudioSystem.STREAM_RING) {
+            flags &= ~AudioManager.FLAG_PLAY_SOUND;
+        }
+
+        // For notifications/ring, show the ui before making any adjustments
+        // Don't suppress mute/unmute requests
+        // Don't suppress adjustments for single volume device
+        if (mVolumeController.suppressAdjustment(resolvedStream, flags, isMute)
+                && !mIsSingleVolume) {
+            direction = 0;
+            flags &= ~AudioManager.FLAG_PLAY_SOUND;
+            flags &= ~AudioManager.FLAG_VIBRATE;
+            if (DEBUG_VOL) Log.d(TAG, "Volume controller suppressed adjustment");
+        }
+
+        adjustStreamVolume(streamType, direction, flags, callingPackage, caller, uid);
+    }
+
+    /** @see AudioManager#adjustStreamVolume(int, int, int) */
+    public void adjustStreamVolume(int streamType, int direction, int flags,
+            String callingPackage) {
+        if ((streamType == AudioManager.STREAM_ACCESSIBILITY) && !canChangeAccessibilityVolume()) {
+            Log.w(TAG, "Trying to call adjustStreamVolume() for a11y without"
+                    + "CHANGE_ACCESSIBILITY_VOLUME / callingPackage=" + callingPackage);
+            return;
+        }
+        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_STREAM_VOL, streamType,
+                direction/*val1*/, flags/*val2*/, callingPackage));
+        adjustStreamVolume(streamType, direction, flags, callingPackage, callingPackage,
+                Binder.getCallingUid());
+    }
+
+    protected void adjustStreamVolume(int streamType, int direction, int flags,
+            String callingPackage, String caller, int uid) {
+        if (mUseFixedVolume) {
+            return;
+        }
+        if (DEBUG_VOL) Log.d(TAG, "adjustStreamVolume() stream=" + streamType + ", dir=" + direction
+                + ", flags=" + flags + ", caller=" + caller);
+
+        ensureValidDirection(direction);
+        ensureValidStreamType(streamType);
+
+        boolean isMuteAdjust = isMuteAdjust(direction);
+
+        if (isMuteAdjust && !isStreamAffectedByMute(streamType)) {
+            return;
+        }
+
+        // If adjust is mute and the stream is STREAM_VOICE_CALL or STREAM_BLUETOOTH_SCO, make sure
+        // that the calling app have the MODIFY_PHONE_STATE permission.
+        if (isMuteAdjust &&
+            (streamType == AudioSystem.STREAM_VOICE_CALL ||
+                streamType == AudioSystem.STREAM_BLUETOOTH_SCO) &&
+            mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE)
+                    != PackageManager.PERMISSION_GRANTED) {
+            Log.w(TAG, "MODIFY_PHONE_STATE Permission Denial: adjustStreamVolume from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+
+        // If the stream is STREAM_ASSISTANT,
+        // make sure that the calling app have the MODIFY_AUDIO_ROUTING permission.
+        if (streamType == AudioSystem.STREAM_ASSISTANT &&
+            mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+                    != PackageManager.PERMISSION_GRANTED) {
+            Log.w(TAG, "MODIFY_AUDIO_ROUTING Permission Denial: adjustStreamVolume from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+
+        // use stream type alias here so that streams with same alias have the same behavior,
+        // including with regard to silent mode control (e.g the use of STREAM_RING below and in
+        // checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION)
+        int streamTypeAlias = mStreamVolumeAlias[streamType];
+
+        VolumeStreamState streamState = mStreamStates[streamTypeAlias];
+
+        final int device = getDeviceForStream(streamTypeAlias);
+
+        int aliasIndex = streamState.getIndex(device);
+        boolean adjustVolume = true;
+        int step;
+
+        // skip a2dp absolute volume control request when the device
+        // is not an a2dp device
+        if (!AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
+                && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) {
+            return;
+        }
+
+        // If we are being called by the system (e.g. hardware keys) check for current user
+        // so we handle user restrictions correctly.
+        if (uid == android.os.Process.SYSTEM_UID) {
+            uid = UserHandle.getUid(getCurrentUserId(), UserHandle.getAppId(uid));
+        }
+        if (mAppOps.noteOp(STREAM_VOLUME_OPS[streamTypeAlias], uid, callingPackage)
+                != AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+
+        // reset any pending volume command
+        synchronized (mSafeMediaVolumeStateLock) {
+            mPendingVolumeCommand = null;
+        }
+
+        flags &= ~AudioManager.FLAG_FIXED_VOLUME;
+        if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
+            flags |= AudioManager.FLAG_FIXED_VOLUME;
+
+            // Always toggle between max safe volume and 0 for fixed volume devices where safe
+            // volume is enforced, and max and 0 for the others.
+            // This is simulated by stepping by the full allowed volume range
+            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
+                    mSafeMediaVolumeDevices.contains(device)) {
+                step = safeMediaVolumeIndex(device);
+            } else {
+                step = streamState.getMaxIndex();
+            }
+            if (aliasIndex != 0) {
+                aliasIndex = step;
+            }
+        } else {
+            // convert one UI step (+/-1) into a number of internal units on the stream alias
+            step = rescaleStep(10, streamType, streamTypeAlias);
+        }
+
+        // If either the client forces allowing ringer modes for this adjustment,
+        // or the stream type is one that is affected by ringer modes
+        if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
+                (streamTypeAlias == getUiSoundsStreamType())) {
+            int ringerMode = getRingerModeInternal();
+            // do not vibrate if already in vibrate mode
+            if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
+                flags &= ~AudioManager.FLAG_VIBRATE;
+            }
+            // Check if the ringer mode handles this adjustment. If it does we don't
+            // need to adjust the volume further.
+            final int result = checkForRingerModeChange(aliasIndex, direction, step,
+                    streamState.mIsMuted, callingPackage, flags);
+            adjustVolume = (result & FLAG_ADJUST_VOLUME) != 0;
+            // If suppressing a volume adjustment in silent mode, display the UI hint
+            if ((result & AudioManager.FLAG_SHOW_SILENT_HINT) != 0) {
+                flags |= AudioManager.FLAG_SHOW_SILENT_HINT;
+            }
+            // If suppressing a volume down adjustment in vibrate mode, display the UI hint
+            if ((result & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0) {
+                flags |= AudioManager.FLAG_SHOW_VIBRATE_HINT;
+            }
+        }
+
+        // If the ringer mode or zen is muting the stream, do not change stream unless
+        // it'll cause us to exit dnd
+        if (!volumeAdjustmentAllowedByDnd(streamTypeAlias, flags)) {
+            adjustVolume = false;
+        }
+        int oldIndex = mStreamStates[streamType].getIndex(device);
+
+        if (adjustVolume && (direction != AudioManager.ADJUST_SAME)) {
+            mAudioHandler.removeMessages(MSG_UNMUTE_STREAM);
+
+            if (isMuteAdjust) {
+                boolean state;
+                if (direction == AudioManager.ADJUST_TOGGLE_MUTE) {
+                    state = !streamState.mIsMuted;
+                } else {
+                    state = direction == AudioManager.ADJUST_MUTE;
+                }
+                if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
+                    setSystemAudioMute(state);
+                }
+                for (int stream = 0; stream < mStreamStates.length; stream++) {
+                    if (streamTypeAlias == mStreamVolumeAlias[stream]) {
+                        if (!(readCameraSoundForced()
+                                    && (mStreamStates[stream].getStreamType()
+                                        == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
+                            mStreamStates[stream].mute(state);
+                        }
+                    }
+                }
+            } else if ((direction == AudioManager.ADJUST_RAISE) &&
+                    !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
+                Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
+                mVolumeController.postDisplaySafeVolumeWarning(flags);
+            } else if (!isFullVolumeDevice(device)
+                    && (streamState.adjustIndex(direction * step, device, caller)
+                            || streamState.mIsMuted)) {
+                // Post message to set system volume (it in turn will post a
+                // message to persist).
+                if (streamState.mIsMuted) {
+                    // Unmute the stream if it was previously muted
+                    if (direction == AudioManager.ADJUST_RAISE) {
+                        // unmute immediately for volume up
+                        streamState.mute(false);
+                    } else if (direction == AudioManager.ADJUST_LOWER) {
+                        if (mIsSingleVolume) {
+                            sendMsg(mAudioHandler, MSG_UNMUTE_STREAM, SENDMSG_QUEUE,
+                                    streamTypeAlias, flags, null, UNMUTE_STREAM_DELAY);
+                        }
+                    }
+                }
+                sendMsg(mAudioHandler,
+                        MSG_SET_DEVICE_VOLUME,
+                        SENDMSG_QUEUE,
+                        device,
+                        0,
+                        streamState,
+                        0);
+            }
+
+            int newIndex = mStreamStates[streamType].getIndex(device);
+
+            // Check if volume update should be send to AVRCP
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+                    && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
+                    && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
+                if (DEBUG_VOL) {
+                    Log.d(TAG, "adjustSreamVolume: postSetAvrcpAbsoluteVolumeIndex index="
+                            + newIndex + "stream=" + streamType);
+                }
+                mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(newIndex / 10);
+            }
+
+            // Check if volume update should be send to Hearing Aid
+            if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
+                // only modify the hearing aid attenuation when the stream to modify matches
+                // the one expected by the hearing aid
+                if (streamType == getHearingAidStreamType()) {
+                    if (DEBUG_VOL) {
+                        Log.d(TAG, "adjustSreamVolume postSetHearingAidVolumeIndex index="
+                                + newIndex + " stream=" + streamType);
+                    }
+                    mDeviceBroker.postSetHearingAidVolumeIndex(newIndex, streamType);
+                }
+            }
+
+            // Check if volume update should be sent to Hdmi system audio.
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
+                setSystemAudioVolume(oldIndex, newIndex, getStreamMaxVolume(streamType), flags);
+            }
+            synchronized (mHdmiClientLock) {
+                if (mHdmiManager != null) {
+                    // mHdmiCecSink true => mHdmiPlaybackClient != null
+                    if (mHdmiCecSink
+                            && streamTypeAlias == AudioSystem.STREAM_MUSIC
+                            // vol change on a full volume device
+                            && isFullVolumeDevice(device)) {
+                        int keyCode = KeyEvent.KEYCODE_UNKNOWN;
+                        switch (direction) {
+                            case AudioManager.ADJUST_RAISE:
+                                keyCode = KeyEvent.KEYCODE_VOLUME_UP;
+                                break;
+                            case AudioManager.ADJUST_LOWER:
+                                keyCode = KeyEvent.KEYCODE_VOLUME_DOWN;
+                                break;
+                            case AudioManager.ADJUST_TOGGLE_MUTE:
+                                keyCode = KeyEvent.KEYCODE_VOLUME_MUTE;
+                                break;
+                            default:
+                                break;
+                        }
+                        if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
+                            final long ident = Binder.clearCallingIdentity();
+                            try {
+                                mHdmiPlaybackClient.sendVolumeKeyEvent(keyCode, true);
+                                mHdmiPlaybackClient.sendVolumeKeyEvent(keyCode, false);
+                            } finally {
+                                Binder.restoreCallingIdentity(ident);
+                            }
+                        }
+                    }
+
+                    if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+                            && (oldIndex != newIndex || isMuteAdjust)) {
+                        maybeSendSystemAudioStatusCommand(isMuteAdjust);
+                    }
+                }
+            }
+        }
+        int index = mStreamStates[streamType].getIndex(device);
+        sendVolumeUpdate(streamType, oldIndex, index, flags, device);
+    }
+
+    // Called after a delay when volume down is pressed while muted
+    private void onUnmuteStream(int stream, int flags) {
+        boolean wasMuted;
+        synchronized (VolumeStreamState.class) {
+            final VolumeStreamState streamState = mStreamStates[stream];
+            wasMuted = streamState.mute(false); // if unmuting causes a change, it was muted
+
+            final int device = getDeviceForStream(stream);
+            final int index = streamState.getIndex(device);
+            sendVolumeUpdate(stream, index, index, flags, device);
+        }
+        if (stream == AudioSystem.STREAM_MUSIC && wasMuted) {
+            synchronized (mHdmiClientLock) {
+                maybeSendSystemAudioStatusCommand(true);
+            }
+        }
+    }
+
+    @GuardedBy("mHdmiClientLock")
+    private void maybeSendSystemAudioStatusCommand(boolean isMuteAdjust) {
+        if (mHdmiAudioSystemClient == null
+                || !mHdmiSystemAudioSupported) {
+            return;
+        }
+
+        final long identity = Binder.clearCallingIdentity();
+        mHdmiAudioSystemClient.sendReportAudioStatusCecCommand(
+                isMuteAdjust, getStreamVolume(AudioSystem.STREAM_MUSIC),
+                getStreamMaxVolume(AudioSystem.STREAM_MUSIC),
+                isStreamMute(AudioSystem.STREAM_MUSIC));
+        Binder.restoreCallingIdentity(identity);
+    }
+
+    private void setSystemAudioVolume(int oldVolume, int newVolume, int maxVolume, int flags) {
+        // Sets the audio volume of AVR when we are in system audio mode. The new volume info
+        // is tranformed to HDMI-CEC commands and passed through CEC bus.
+        synchronized (mHdmiClientLock) {
+            if (mHdmiManager == null
+                    || mHdmiTvClient == null
+                    || oldVolume == newVolume
+                    || (flags & AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME) != 0
+                    || !mHdmiSystemAudioSupported) {
+                return;
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mHdmiTvClient.setSystemAudioVolume(oldVolume, newVolume, maxVolume);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
+
+    // StreamVolumeCommand contains the information needed to defer the process of
+    // setStreamVolume() in case the user has to acknowledge the safe volume warning message.
+    class StreamVolumeCommand {
+        public final int mStreamType;
+        public final int mIndex;
+        public final int mFlags;
+        public final int mDevice;
+
+        StreamVolumeCommand(int streamType, int index, int flags, int device) {
+            mStreamType = streamType;
+            mIndex = index;
+            mFlags = flags;
+            mDevice = device;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder().append("{streamType=").append(mStreamType).append(",index=")
+                    .append(mIndex).append(",flags=").append(mFlags).append(",device=")
+                    .append(mDevice).append('}').toString();
+        }
+    };
+
+    private int getNewRingerMode(int stream, int index, int flags) {
+        // setRingerMode does nothing if the device is single volume,so the value would be unchanged
+        if (mIsSingleVolume) {
+            return getRingerModeExternal();
+        }
+
+        // setting volume on ui sounds stream type also controls silent mode
+        if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
+                (stream == getUiSoundsStreamType())) {
+            int newRingerMode;
+            if (index == 0) {
+                newRingerMode = mHasVibrator ? AudioManager.RINGER_MODE_VIBRATE
+                        : mVolumePolicy.volumeDownToEnterSilent ? AudioManager.RINGER_MODE_SILENT
+                                : AudioManager.RINGER_MODE_NORMAL;
+            } else {
+                newRingerMode = AudioManager.RINGER_MODE_NORMAL;
+            }
+            return newRingerMode;
+        }
+        return getRingerModeExternal();
+    }
+
+    private boolean isAndroidNPlus(String caller) {
+        try {
+            final ApplicationInfo applicationInfo =
+                    mContext.getPackageManager().getApplicationInfoAsUser(
+                            caller, 0, UserHandle.getUserId(Binder.getCallingUid()));
+            if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.N) {
+                return true;
+            }
+            return false;
+        } catch (PackageManager.NameNotFoundException e) {
+            return true;
+        }
+    }
+
+    private boolean wouldToggleZenMode(int newMode) {
+        if (getRingerModeExternal() == AudioManager.RINGER_MODE_SILENT
+                && newMode != AudioManager.RINGER_MODE_SILENT) {
+            return true;
+        } else if (getRingerModeExternal() != AudioManager.RINGER_MODE_SILENT
+                && newMode == AudioManager.RINGER_MODE_SILENT) {
+            return true;
+        }
+        return false;
+    }
+
+    private void onSetStreamVolume(int streamType, int index, int flags, int device,
+            String caller) {
+        final int stream = mStreamVolumeAlias[streamType];
+        setStreamVolumeInt(stream, index, device, false, caller);
+        // setting volume on ui sounds stream type also controls silent mode
+        if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
+                (stream == getUiSoundsStreamType())) {
+            setRingerMode(getNewRingerMode(stream, index, flags),
+                    TAG + ".onSetStreamVolume", false /*external*/);
+        }
+        // setting non-zero volume for a muted stream unmutes the stream and vice versa,
+        // except for BT SCO stream where only explicit mute is allowed to comply to BT requirements
+        if (streamType != AudioSystem.STREAM_BLUETOOTH_SCO) {
+            mStreamStates[stream].mute(index == 0);
+        }
+    }
+
+    private void enforceModifyAudioRoutingPermission() {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Missing MODIFY_AUDIO_ROUTING permission");
+        }
+    }
+
+    /** @see AudioManager#setVolumeIndexForAttributes(attr, int, int) */
+    public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags,
+                                            String callingPackage) {
+        enforceModifyAudioRoutingPermission();
+        Objects.requireNonNull(attr, "attr must not be null");
+        final int volumeGroup = getVolumeGroupIdForAttributes(attr);
+        if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) {
+            Log.e(TAG, ": no volume group found for attributes " + attr.toString());
+            return;
+        }
+        final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup);
+
+        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(),
+                index/*val1*/, flags/*val2*/, callingPackage));
+
+        vgs.setVolumeIndex(index, flags);
+
+        // For legacy reason, propagate to all streams associated to this volume group
+        for (final int groupedStream : vgs.getLegacyStreamTypes()) {
+            try {
+                ensureValidStreamType(groupedStream);
+            } catch (IllegalArgumentException e) {
+                Log.d(TAG, "volume group " + volumeGroup + " has internal streams (" + groupedStream
+                        + "), do not change associated stream volume");
+                continue;
+            }
+            setStreamVolume(groupedStream, index, flags, callingPackage, callingPackage,
+                            Binder.getCallingUid());
+        }
+    }
+
+    @Nullable
+    private AudioVolumeGroup getAudioVolumeGroupById(int volumeGroupId) {
+        for (final AudioVolumeGroup avg : AudioVolumeGroup.getAudioVolumeGroups()) {
+            if (avg.getId() == volumeGroupId) {
+                return avg;
+            }
+        }
+
+        Log.e(TAG, ": invalid volume group id: " + volumeGroupId + " requested");
+        return null;
+    }
+
+    /** @see AudioManager#getVolumeIndexForAttributes(attr) */
+    public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+        enforceModifyAudioRoutingPermission();
+        Objects.requireNonNull(attr, "attr must not be null");
+        final int volumeGroup = getVolumeGroupIdForAttributes(attr);
+        if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) {
+            throw new IllegalArgumentException("No volume group for attributes " + attr);
+        }
+        final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup);
+        return vgs.getVolumeIndex();
+    }
+
+    /** @see AudioManager#getMaxVolumeIndexForAttributes(attr) */
+    public int getMaxVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+        enforceModifyAudioRoutingPermission();
+        Objects.requireNonNull(attr, "attr must not be null");
+        return AudioSystem.getMaxVolumeIndexForAttributes(attr);
+    }
+
+    /** @see AudioManager#getMinVolumeIndexForAttributes(attr) */
+    public int getMinVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+        enforceModifyAudioRoutingPermission();
+        Objects.requireNonNull(attr, "attr must not be null");
+        return AudioSystem.getMinVolumeIndexForAttributes(attr);
+    }
+
+    /** @see AudioManager#setStreamVolume(int, int, int) */
+    public void setStreamVolume(int streamType, int index, int flags, String callingPackage) {
+        if ((streamType == AudioManager.STREAM_ACCESSIBILITY) && !canChangeAccessibilityVolume()) {
+            Log.w(TAG, "Trying to call setStreamVolume() for a11y without"
+                    + " CHANGE_ACCESSIBILITY_VOLUME  callingPackage=" + callingPackage);
+            return;
+        }
+        if ((streamType == AudioManager.STREAM_VOICE_CALL) && (index == 0)
+                && (mContext.checkCallingOrSelfPermission(
+                    android.Manifest.permission.MODIFY_PHONE_STATE)
+                    != PackageManager.PERMISSION_GRANTED)) {
+            Log.w(TAG, "Trying to call setStreamVolume() for STREAM_VOICE_CALL and index 0 without"
+                    + " MODIFY_PHONE_STATE  callingPackage=" + callingPackage);
+            return;
+        }
+        if ((streamType == AudioManager.STREAM_ASSISTANT)
+            && (mContext.checkCallingOrSelfPermission(
+                    android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+                    != PackageManager.PERMISSION_GRANTED)) {
+            Log.w(TAG, "Trying to call setStreamVolume() for STREAM_ASSISTANT without"
+                    + " MODIFY_AUDIO_ROUTING  callingPackage=" + callingPackage);
+            return;
+        }
+        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
+                index/*val1*/, flags/*val2*/, callingPackage));
+        setStreamVolume(streamType, index, flags, callingPackage, callingPackage,
+                Binder.getCallingUid());
+    }
+
+    private boolean canChangeAccessibilityVolume() {
+        synchronized (mAccessibilityServiceUidsLock) {
+            if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+                    android.Manifest.permission.CHANGE_ACCESSIBILITY_VOLUME)) {
+                return true;
+            }
+            if (mAccessibilityServiceUids != null) {
+                int callingUid = Binder.getCallingUid();
+                for (int i = 0; i < mAccessibilityServiceUids.length; i++) {
+                    if (mAccessibilityServiceUids[i] == callingUid) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+    /*package*/ int getHearingAidStreamType() {
+        return getHearingAidStreamType(mMode);
+    }
+
+    private int getHearingAidStreamType(int mode) {
+        switch (mode) {
+            case AudioSystem.MODE_IN_COMMUNICATION:
+            case AudioSystem.MODE_IN_CALL:
+                return AudioSystem.STREAM_VOICE_CALL;
+            case AudioSystem.MODE_NORMAL:
+            default:
+                // other conditions will influence the stream type choice, read on...
+                break;
+        }
+        if (mVoiceActive.get()) {
+            return AudioSystem.STREAM_VOICE_CALL;
+        }
+        return AudioSystem.STREAM_MUSIC;
+    }
+
+    private AtomicBoolean mVoiceActive = new AtomicBoolean(false);
+
+    private final IPlaybackConfigDispatcher mVoiceActivityMonitor =
+            new IPlaybackConfigDispatcher.Stub() {
+        @Override
+        public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs,
+                                                 boolean flush) {
+            sendMsg(mAudioHandler, MSG_PLAYBACK_CONFIG_CHANGE, SENDMSG_REPLACE,
+                    0 /*arg1 ignored*/, 0 /*arg2 ignored*/,
+                    configs /*obj*/, 0 /*delay*/);
+        }
+    };
+
+    private void onPlaybackConfigChange(List<AudioPlaybackConfiguration> configs) {
+        boolean voiceActive = false;
+        for (AudioPlaybackConfiguration config : configs) {
+            final int usage = config.getAudioAttributes().getUsage();
+            if ((usage == AudioAttributes.USAGE_VOICE_COMMUNICATION
+                    || usage == AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
+                    && config.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                voiceActive = true;
+                break;
+            }
+        }
+        if (mVoiceActive.getAndSet(voiceActive) != voiceActive) {
+            updateHearingAidVolumeOnVoiceActivityUpdate();
+        }
+    }
+
+    private void updateHearingAidVolumeOnVoiceActivityUpdate() {
+        final int streamType = getHearingAidStreamType();
+        final int index = getStreamVolume(streamType);
+        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_VOICE_ACTIVITY_HEARING_AID,
+                mVoiceActive.get(), streamType, index));
+        mDeviceBroker.postSetHearingAidVolumeIndex(index * 10, streamType);
+
+    }
+
+    /**
+     * Manage an audio mode change for audio devices that use an "absolute volume" model,
+     * i.e. the framework sends the full scale signal, and the actual volume for the use case
+     * is communicated separately.
+     */
+    void updateAbsVolumeMultiModeDevices(int oldMode, int newMode) {
+        if (oldMode == newMode) {
+            return;
+        }
+        switch (newMode) {
+            case AudioSystem.MODE_IN_COMMUNICATION:
+            case AudioSystem.MODE_IN_CALL:
+            case AudioSystem.MODE_NORMAL:
+                break;
+            case AudioSystem.MODE_RINGTONE:
+                // not changing anything for ringtone
+                return;
+            case AudioSystem.MODE_CURRENT:
+            case AudioSystem.MODE_INVALID:
+            default:
+                // don't know what to do in this case, better bail
+                return;
+        }
+
+        int streamType = getHearingAidStreamType(newMode);
+
+        final Set<Integer> deviceTypes = AudioSystem.generateAudioDeviceTypesSet(
+                AudioSystem.getDevicesForStream(streamType));
+        final Set<Integer> absVolumeMultiModeCaseDevices = AudioSystem.intersectionAudioDeviceTypes(
+                mAbsVolumeMultiModeCaseDevices, deviceTypes);
+        if (absVolumeMultiModeCaseDevices.isEmpty()) {
+            return;
+        }
+
+        // handling of specific interfaces goes here:
+        if (AudioSystem.isSingleAudioDeviceType(
+                absVolumeMultiModeCaseDevices, AudioSystem.DEVICE_OUT_HEARING_AID)) {
+            final int index = getStreamVolume(streamType);
+            sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_MODE_CHANGE_HEARING_AID,
+                    newMode, streamType, index));
+            mDeviceBroker.postSetHearingAidVolumeIndex(index * 10, streamType);
+        }
+    }
+
+    private void setStreamVolume(int streamType, int index, int flags, String callingPackage,
+            String caller, int uid) {
+        if (DEBUG_VOL) {
+            Log.d(TAG, "setStreamVolume(stream=" + streamType+", index=" + index
+                    + ", calling=" + callingPackage + ")");
+        }
+        if (mUseFixedVolume) {
+            return;
+        }
+
+        ensureValidStreamType(streamType);
+        int streamTypeAlias = mStreamVolumeAlias[streamType];
+        VolumeStreamState streamState = mStreamStates[streamTypeAlias];
+
+        final int device = getDeviceForStream(streamType);
+        int oldIndex;
+
+        // skip a2dp absolute volume control request when the device
+        // is not an a2dp device
+        if (!AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
+                && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) {
+            return;
+        }
+        // If we are being called by the system (e.g. hardware keys) check for current user
+        // so we handle user restrictions correctly.
+        if (uid == android.os.Process.SYSTEM_UID) {
+            uid = UserHandle.getUid(getCurrentUserId(), UserHandle.getAppId(uid));
+        }
+        if (mAppOps.noteOp(STREAM_VOLUME_OPS[streamTypeAlias], uid, callingPackage)
+                != AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+
+        if (isAndroidNPlus(callingPackage)
+                && wouldToggleZenMode(getNewRingerMode(streamTypeAlias, index, flags))
+                && !mNm.isNotificationPolicyAccessGrantedForPackage(callingPackage)) {
+            throw new SecurityException("Not allowed to change Do Not Disturb state");
+        }
+
+        if (!volumeAdjustmentAllowedByDnd(streamTypeAlias, flags)) {
+            return;
+        }
+
+        synchronized (mSafeMediaVolumeStateLock) {
+            // reset any pending volume command
+            mPendingVolumeCommand = null;
+
+            oldIndex = streamState.getIndex(device);
+
+            index = rescaleIndex(index * 10, streamType, streamTypeAlias);
+
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+                    && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
+                    && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
+                if (DEBUG_VOL) {
+                    Log.d(TAG, "setStreamVolume postSetAvrcpAbsoluteVolumeIndex index=" + index
+                            + "stream=" + streamType);
+                }
+                mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(index / 10);
+            }
+
+            if (device == AudioSystem.DEVICE_OUT_HEARING_AID
+                    && streamType == getHearingAidStreamType()) {
+                Log.i(TAG, "setStreamVolume postSetHearingAidVolumeIndex index=" + index
+                        + " stream=" + streamType);
+                mDeviceBroker.postSetHearingAidVolumeIndex(index, streamType);
+            }
+
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
+                setSystemAudioVolume(oldIndex, index, getStreamMaxVolume(streamType), flags);
+            }
+
+            flags &= ~AudioManager.FLAG_FIXED_VOLUME;
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
+                flags |= AudioManager.FLAG_FIXED_VOLUME;
+
+                // volume is either 0 or max allowed for fixed volume devices
+                if (index != 0) {
+                    if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
+                            mSafeMediaVolumeDevices.contains(device)) {
+                        index = safeMediaVolumeIndex(device);
+                    } else {
+                        index = streamState.getMaxIndex();
+                    }
+                }
+            }
+
+            if (!checkSafeMediaVolume(streamTypeAlias, index, device)) {
+                mVolumeController.postDisplaySafeVolumeWarning(flags);
+                mPendingVolumeCommand = new StreamVolumeCommand(
+                                                    streamType, index, flags, device);
+            } else {
+                onSetStreamVolume(streamType, index, flags, device, caller);
+                index = mStreamStates[streamType].getIndex(device);
+            }
+        }
+        synchronized (mHdmiClientLock) {
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+                    && (oldIndex != index)) {
+                maybeSendSystemAudioStatusCommand(false);
+            }
+        }
+        sendVolumeUpdate(streamType, oldIndex, index, flags, device);
+    }
+
+
+
+    private int getVolumeGroupIdForAttributes(@NonNull AudioAttributes attributes) {
+        Objects.requireNonNull(attributes, "attributes must not be null");
+        int volumeGroupId = getVolumeGroupIdForAttributesInt(attributes);
+        if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
+            return volumeGroupId;
+        }
+        // The default volume group is the one hosted by default product strategy, i.e.
+        // supporting Default Attributes
+        return getVolumeGroupIdForAttributesInt(AudioProductStrategy.sDefaultAttributes);
+    }
+
+    private int getVolumeGroupIdForAttributesInt(@NonNull AudioAttributes attributes) {
+        Objects.requireNonNull(attributes, "attributes must not be null");
+        for (final AudioProductStrategy productStrategy :
+                AudioProductStrategy.getAudioProductStrategies()) {
+            int volumeGroupId = productStrategy.getVolumeGroupIdForAudioAttributes(attributes);
+            if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
+                return volumeGroupId;
+            }
+        }
+        return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
+    }
+
+
+    // No ringer or zen muted stream volumes can be changed unless it'll exit dnd
+    private boolean volumeAdjustmentAllowedByDnd(int streamTypeAlias, int flags) {
+        switch (mNm.getZenMode()) {
+            case Settings.Global.ZEN_MODE_OFF:
+                return true;
+            case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS:
+            case Settings.Global.ZEN_MODE_ALARMS:
+            case Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
+                return !isStreamMutedByRingerOrZenMode(streamTypeAlias)
+                        || streamTypeAlias == getUiSoundsStreamType()
+                        || (flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0;
+        }
+
+        return true;
+    }
+
+    /** @see AudioManager#forceVolumeControlStream(int) */
+    public void forceVolumeControlStream(int streamType, IBinder cb) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+                != PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+        if (DEBUG_VOL) { Log.d(TAG, String.format("forceVolumeControlStream(%d)", streamType)); }
+        synchronized(mForceControlStreamLock) {
+            if (mVolumeControlStream != -1 && streamType != -1) {
+                mUserSelectedVolumeControlStream = true;
+            }
+            mVolumeControlStream = streamType;
+            if (mVolumeControlStream == -1) {
+                if (mForceControlStreamClient != null) {
+                    mForceControlStreamClient.release();
+                    mForceControlStreamClient = null;
+                }
+                mUserSelectedVolumeControlStream = false;
+            } else {
+                if (null == mForceControlStreamClient) {
+                    mForceControlStreamClient = new ForceControlStreamClient(cb);
+                } else {
+                    if (mForceControlStreamClient.getBinder() == cb) {
+                        Log.d(TAG, "forceVolumeControlStream cb:" + cb + " is already linked.");
+                    } else {
+                        mForceControlStreamClient.release();
+                        mForceControlStreamClient = new ForceControlStreamClient(cb);
+                    }
+                }
+            }
+        }
+    }
+
+    private class ForceControlStreamClient implements IBinder.DeathRecipient {
+        private IBinder mCb; // To be notified of client's death
+
+        ForceControlStreamClient(IBinder cb) {
+            if (cb != null) {
+                try {
+                    cb.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    // Client has died!
+                    Log.w(TAG, "ForceControlStreamClient() could not link to "+cb+" binder death");
+                    cb = null;
+                }
+            }
+            mCb = cb;
+        }
+
+        public void binderDied() {
+            synchronized(mForceControlStreamLock) {
+                Log.w(TAG, "SCO client died");
+                if (mForceControlStreamClient != this) {
+                    Log.w(TAG, "unregistered control stream client died");
+                } else {
+                    mForceControlStreamClient = null;
+                    mVolumeControlStream = -1;
+                    mUserSelectedVolumeControlStream = false;
+                }
+            }
+        }
+
+        public void release() {
+            if (mCb != null) {
+                mCb.unlinkToDeath(this, 0);
+                mCb = null;
+            }
+        }
+
+        public IBinder getBinder() {
+            return mCb;
+        }
+    }
+
+    private void sendBroadcastToAll(Intent intent) {
+        if (!mSystemServer.isPrivileged()) {
+            return;
+        }
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void sendStickyBroadcastToAll(Intent intent) {
+        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private int getCurrentUserId() {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            UserInfo currentUser = ActivityManager.getService().getCurrentUser();
+            return currentUser.id;
+        } catch (RemoteException e) {
+            // Activity manager not running, nothing we can do assume user 0.
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+        return UserHandle.USER_SYSTEM;
+    }
+
+    // UI update and Broadcast Intent
+    protected void sendVolumeUpdate(int streamType, int oldIndex, int index, int flags, int device)
+    {
+        streamType = mStreamVolumeAlias[streamType];
+
+        if (streamType == AudioSystem.STREAM_MUSIC) {
+            flags = updateFlagsForTvPlatform(flags);
+            if (isFullVolumeDevice(device)) {
+                flags &= ~AudioManager.FLAG_SHOW_UI;
+            }
+        }
+        mVolumeController.postVolumeChanged(streamType, flags);
+    }
+
+    // If Hdmi-CEC system audio mode is on and we are a TV panel, never show volume bar.
+    private int updateFlagsForTvPlatform(int flags) {
+        synchronized (mHdmiClientLock) {
+            if (mHdmiTvClient != null && mHdmiSystemAudioSupported) {
+                flags &= ~AudioManager.FLAG_SHOW_UI;
+            }
+        }
+        return flags;
+    }
+
+    // UI update and Broadcast Intent
+    private void sendMasterMuteUpdate(boolean muted, int flags) {
+        mVolumeController.postMasterMuteChanged(updateFlagsForTvPlatform(flags));
+        broadcastMasterMuteStatus(muted);
+    }
+
+    private void broadcastMasterMuteStatus(boolean muted) {
+        Intent intent = new Intent(AudioManager.MASTER_MUTE_CHANGED_ACTION);
+        intent.putExtra(AudioManager.EXTRA_MASTER_VOLUME_MUTED, muted);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+                | Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        sendStickyBroadcastToAll(intent);
+    }
+
+    /**
+     * Sets the stream state's index, and posts a message to set system volume.
+     * This will not call out to the UI. Assumes a valid stream type.
+     *
+     * @param streamType Type of the stream
+     * @param index Desired volume index of the stream
+     * @param device the device whose volume must be changed
+     * @param force If true, set the volume even if the desired volume is same
+     * as the current volume.
+     */
+    private void setStreamVolumeInt(int streamType,
+                                    int index,
+                                    int device,
+                                    boolean force,
+                                    String caller) {
+        if (isFullVolumeDevice(device)) {
+            return;
+        }
+        VolumeStreamState streamState = mStreamStates[streamType];
+
+        if (streamState.setIndex(index, device, caller) || force) {
+            // Post message to set system volume (it in turn will post a message
+            // to persist).
+            sendMsg(mAudioHandler,
+                    MSG_SET_DEVICE_VOLUME,
+                    SENDMSG_QUEUE,
+                    device,
+                    0,
+                    streamState,
+                    0);
+        }
+    }
+
+    private void setSystemAudioMute(boolean state) {
+        synchronized (mHdmiClientLock) {
+            if (mHdmiManager == null || mHdmiTvClient == null || !mHdmiSystemAudioSupported) return;
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mHdmiTvClient.setSystemAudioMute(state);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
+
+    /** get stream mute state. */
+    public boolean isStreamMute(int streamType) {
+        if (streamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+            streamType = getActiveStreamType(streamType);
+        }
+        synchronized (VolumeStreamState.class) {
+            ensureValidStreamType(streamType);
+            return mStreamStates[streamType].mIsMuted;
+        }
+    }
+
+    private class RmtSbmxFullVolDeathHandler implements IBinder.DeathRecipient {
+        private IBinder mICallback; // To be notified of client's death
+
+        RmtSbmxFullVolDeathHandler(IBinder cb) {
+            mICallback = cb;
+            try {
+                cb.linkToDeath(this, 0/*flags*/);
+            } catch (RemoteException e) {
+                Log.e(TAG, "can't link to death", e);
+            }
+        }
+
+        boolean isHandlerFor(IBinder cb) {
+            return mICallback.equals(cb);
+        }
+
+        void forget() {
+            try {
+                mICallback.unlinkToDeath(this, 0/*flags*/);
+            } catch (NoSuchElementException e) {
+                Log.e(TAG, "error unlinking to death", e);
+            }
+        }
+
+        public void binderDied() {
+            Log.w(TAG, "Recorder with remote submix at full volume died " + mICallback);
+            forceRemoteSubmixFullVolume(false, mICallback);
+        }
+    }
+
+    /**
+     * call must be synchronized on mRmtSbmxFullVolDeathHandlers
+     * @return true if there is a registered death handler, false otherwise */
+    private boolean discardRmtSbmxFullVolDeathHandlerFor(IBinder cb) {
+        Iterator<RmtSbmxFullVolDeathHandler> it = mRmtSbmxFullVolDeathHandlers.iterator();
+        while (it.hasNext()) {
+            final RmtSbmxFullVolDeathHandler handler = it.next();
+            if (handler.isHandlerFor(cb)) {
+                handler.forget();
+                mRmtSbmxFullVolDeathHandlers.remove(handler);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** call synchronized on mRmtSbmxFullVolDeathHandlers */
+    private boolean hasRmtSbmxFullVolDeathHandlerFor(IBinder cb) {
+        Iterator<RmtSbmxFullVolDeathHandler> it = mRmtSbmxFullVolDeathHandlers.iterator();
+        while (it.hasNext()) {
+            if (it.next().isHandlerFor(cb)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private int mRmtSbmxFullVolRefCount = 0;
+    private ArrayList<RmtSbmxFullVolDeathHandler> mRmtSbmxFullVolDeathHandlers =
+            new ArrayList<RmtSbmxFullVolDeathHandler>();
+
+    public void forceRemoteSubmixFullVolume(boolean startForcing, IBinder cb) {
+        if (cb == null) {
+            return;
+        }
+        if ((PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission(
+                        android.Manifest.permission.CAPTURE_AUDIO_OUTPUT))) {
+            Log.w(TAG, "Trying to call forceRemoteSubmixFullVolume() without CAPTURE_AUDIO_OUTPUT");
+            return;
+        }
+        synchronized(mRmtSbmxFullVolDeathHandlers) {
+            boolean applyRequired = false;
+            if (startForcing) {
+                if (!hasRmtSbmxFullVolDeathHandlerFor(cb)) {
+                    mRmtSbmxFullVolDeathHandlers.add(new RmtSbmxFullVolDeathHandler(cb));
+                    if (mRmtSbmxFullVolRefCount == 0) {
+                        mFullVolumeDevices.add(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX);
+                        mFixedVolumeDevices.add(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX);
+                        applyRequired = true;
+                    }
+                    mRmtSbmxFullVolRefCount++;
+                }
+            } else {
+                if (discardRmtSbmxFullVolDeathHandlerFor(cb) && (mRmtSbmxFullVolRefCount > 0)) {
+                    mRmtSbmxFullVolRefCount--;
+                    if (mRmtSbmxFullVolRefCount == 0) {
+                        mFullVolumeDevices.remove(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX);
+                        mFixedVolumeDevices.remove(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX);
+                        applyRequired = true;
+                    }
+                }
+            }
+            if (applyRequired) {
+                // Assumes only STREAM_MUSIC going through DEVICE_OUT_REMOTE_SUBMIX
+                checkAllFixedVolumeDevices(AudioSystem.STREAM_MUSIC);
+                mStreamStates[AudioSystem.STREAM_MUSIC].applyAllVolumes();
+            }
+        }
+    }
+
+    private void setMasterMuteInternal(boolean mute, int flags, String callingPackage, int uid,
+            int userId) {
+        // If we are being called by the system check for user we are going to change
+        // so we handle user restrictions correctly.
+        if (uid == android.os.Process.SYSTEM_UID) {
+            uid = UserHandle.getUid(userId, UserHandle.getAppId(uid));
+        }
+        // If OP_AUDIO_MASTER_VOLUME is set, disallow unmuting.
+        if (!mute && mAppOps.noteOp(AppOpsManager.OP_AUDIO_MASTER_VOLUME, uid, callingPackage)
+                != AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+        if (userId != UserHandle.getCallingUserId() &&
+                mContext.checkCallingOrSelfPermission(
+                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+                != PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+        setMasterMuteInternalNoCallerCheck(mute, flags, userId);
+    }
+
+    private void setMasterMuteInternalNoCallerCheck(boolean mute, int flags, int userId) {
+        if (DEBUG_VOL) {
+            Log.d(TAG, String.format("Master mute %s, %d, user=%d", mute, flags, userId));
+        }
+        if (!isPlatformAutomotive() && mUseFixedVolume) {
+            // If using fixed volume, we don't mute.
+            // TODO: remove the isPlatformAutomotive check here.
+            // The isPlatformAutomotive check is added for safety but may not be necessary.
+            return;
+        }
+        // For automotive,
+        // - the car service is always running as system user
+        // - foreground users are non-system users
+        // Car service is in charge of dispatching the key event include master mute to Android.
+        // Therefore, the getCurrentUser() is always different to the foreground user.
+        if ((isPlatformAutomotive() && userId == UserHandle.USER_SYSTEM)
+                || (getCurrentUserId() == userId)) {
+            if (mute != AudioSystem.getMasterMute()) {
+                setSystemAudioMute(mute);
+                AudioSystem.setMasterMute(mute);
+                sendMasterMuteUpdate(mute, flags);
+            }
+        }
+    }
+
+    /** get master mute state. */
+    public boolean isMasterMute() {
+        return AudioSystem.getMasterMute();
+    }
+
+    public void setMasterMute(boolean mute, int flags, String callingPackage, int userId) {
+        enforceModifyAudioRoutingPermission();
+        setMasterMuteInternal(mute, flags, callingPackage, Binder.getCallingUid(),
+                userId);
+    }
+
+    /** @see AudioManager#getStreamVolume(int) */
+    public int getStreamVolume(int streamType) {
+        ensureValidStreamType(streamType);
+        int device = getDeviceForStream(streamType);
+        synchronized (VolumeStreamState.class) {
+            int index = mStreamStates[streamType].getIndex(device);
+
+            // by convention getStreamVolume() returns 0 when a stream is muted.
+            if (mStreamStates[streamType].mIsMuted) {
+                index = 0;
+            }
+            if (index != 0 && (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) &&
+                    isFixedVolumeDevice(device)) {
+                index = mStreamStates[streamType].getMaxIndex();
+            }
+            return (index + 5) / 10;
+        }
+    }
+
+    /** @see AudioManager#getStreamMaxVolume(int) */
+    public int getStreamMaxVolume(int streamType) {
+        ensureValidStreamType(streamType);
+        return (mStreamStates[streamType].getMaxIndex() + 5) / 10;
+    }
+
+    /** @see AudioManager#getStreamMinVolumeInt(int) */
+    public int getStreamMinVolume(int streamType) {
+        ensureValidStreamType(streamType);
+        return (mStreamStates[streamType].getMinIndex() + 5) / 10;
+    }
+
+    /** Get last audible volume before stream was muted. */
+    public int getLastAudibleStreamVolume(int streamType) {
+        ensureValidStreamType(streamType);
+        int device = getDeviceForStream(streamType);
+        return (mStreamStates[streamType].getIndex(device) + 5) / 10;
+    }
+
+    /** @see AudioManager#getUiSoundsStreamType()  */
+    public int getUiSoundsStreamType() {
+        return mStreamVolumeAlias[AudioSystem.STREAM_SYSTEM];
+    }
+
+    /** @see AudioManager#setMicrophoneMute(boolean) */
+    @Override
+    public void setMicrophoneMute(boolean on, String callingPackage, int userId) {
+        // If we are being called by the system check for user we are going to change
+        // so we handle user restrictions correctly.
+        int uid = Binder.getCallingUid();
+        if (uid == android.os.Process.SYSTEM_UID) {
+            uid = UserHandle.getUid(userId, UserHandle.getAppId(uid));
+        }
+        MediaMetrics.Item mmi = new MediaMetrics.Item(MediaMetrics.Name.AUDIO_MIC)
+                .setUid(uid)
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackage)
+                .set(MediaMetrics.Property.EVENT, "setMicrophoneMute")
+                .set(MediaMetrics.Property.REQUEST, on
+                        ? MediaMetrics.Value.MUTE : MediaMetrics.Value.UNMUTE);
+
+        // If OP_MUTE_MICROPHONE is set, disallow unmuting.
+        if (!on && mAppOps.noteOp(AppOpsManager.OP_MUTE_MICROPHONE, uid, callingPackage)
+                != AppOpsManager.MODE_ALLOWED) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "disallow unmuting").record();
+            return;
+        }
+        if (!checkAudioSettingsPermission("setMicrophoneMute()")) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "!checkAudioSettingsPermission").record();
+            return;
+        }
+        if (userId != UserHandle.getCallingUserId() &&
+                mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+                != PackageManager.PERMISSION_GRANTED) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "permission").record();
+            return;
+        }
+        mMicMuteFromApi = on;
+        mmi.record(); // record now, the no caller check will set the mute state.
+        setMicrophoneMuteNoCallerCheck(userId);
+    }
+
+    /** @see AudioManager#setMicrophoneMuteFromSwitch(boolean) */
+    public void setMicrophoneMuteFromSwitch(boolean on) {
+        int userId = Binder.getCallingUid();
+        if (userId != android.os.Process.SYSTEM_UID) {
+            Log.e(TAG, "setMicrophoneMuteFromSwitch() called from non system user!");
+            return;
+        }
+        mMicMuteFromSwitch = on;
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_MIC)
+                .setUid(userId)
+                .set(MediaMetrics.Property.EVENT, "setMicrophoneMuteFromSwitch")
+                .set(MediaMetrics.Property.REQUEST, on
+                        ? MediaMetrics.Value.MUTE : MediaMetrics.Value.UNMUTE)
+                .record();
+        setMicrophoneMuteNoCallerCheck(userId);
+    }
+
+    private void setMicMuteFromSwitchInput() {
+        InputManager im = mContext.getSystemService(InputManager.class);
+        final int isMicMuted = im.isMicMuted();
+        if (isMicMuted != InputManager.SWITCH_STATE_UNKNOWN) {
+            setMicrophoneMuteFromSwitch(im.isMicMuted() != InputManager.SWITCH_STATE_OFF);
+        }
+    }
+
+    /**
+     * Returns the microphone mute state as seen from the native audio system
+     * @return true if microphone is reported as muted by primary HAL
+     */
+    public boolean isMicrophoneMuted() {
+        return mMicMuteFromSystemCached;
+    }
+
+    private boolean isMicrophoneSupposedToBeMuted() {
+        return mMicMuteFromSwitch || mMicMuteFromRestrictions || mMicMuteFromApi;
+    }
+
+    private void setMicrophoneMuteNoCallerCheck(int userId) {
+        final boolean muted = isMicrophoneSupposedToBeMuted();
+        if (DEBUG_VOL) {
+            Log.d(TAG, String.format("Mic mute %b, user=%d", muted, userId));
+        }
+        // only mute for the current user
+        if (getCurrentUserId() == userId || userId == android.os.Process.SYSTEM_UID) {
+            final boolean currentMute = mAudioSystem.isMicrophoneMuted();
+            final long identity = Binder.clearCallingIdentity();
+            final int ret = mAudioSystem.muteMicrophone(muted);
+
+            // update cache with the real state independently from what was set
+            mMicMuteFromSystemCached = mAudioSystem.isMicrophoneMuted();
+            if (ret != AudioSystem.AUDIO_STATUS_OK) {
+                Log.e(TAG, "Error changing mic mute state to " + muted + " current:"
+                        + mMicMuteFromSystemCached);
+            }
+
+            new MediaMetrics.Item(MediaMetrics.Name.AUDIO_MIC)
+                    .setUid(userId)
+                    .set(MediaMetrics.Property.EVENT, "setMicrophoneMuteNoCallerCheck")
+                    .set(MediaMetrics.Property.MUTE, mMicMuteFromSystemCached
+                            ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
+                    .set(MediaMetrics.Property.REQUEST, muted
+                            ? MediaMetrics.Value.MUTE : MediaMetrics.Value.UNMUTE)
+                    .set(MediaMetrics.Property.STATUS, ret)
+                    .record();
+
+            try {
+                // send the intent even if there was a failure to change the actual mute state:
+                // the AudioManager.setMicrophoneMute API doesn't have a return value to
+                // indicate if the call failed to successfully change the mute state, and receiving
+                // the intent may be the only time an application can resynchronize its mic mute
+                // state with the actual system mic mute state
+                if (muted != currentMute) {
+                    sendMsg(mAudioHandler, MSG_BROADCAST_MICROPHONE_MUTE,
+                                SENDMSG_NOOP, 0, 0, null, 0);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    @Override
+    public int getRingerModeExternal() {
+        synchronized(mSettingsLock) {
+            return mRingerModeExternal;
+        }
+    }
+
+    @Override
+    public int getRingerModeInternal() {
+        synchronized(mSettingsLock) {
+            return mRingerMode;
+        }
+    }
+
+    private void ensureValidRingerMode(int ringerMode) {
+        if (!isValidRingerMode(ringerMode)) {
+            throw new IllegalArgumentException("Bad ringer mode " + ringerMode);
+        }
+    }
+
+    /** @see AudioManager#isValidRingerMode(int) */
+    public boolean isValidRingerMode(int ringerMode) {
+        return ringerMode >= 0 && ringerMode <= AudioManager.RINGER_MODE_MAX;
+    }
+
+    public void setRingerModeExternal(int ringerMode, String caller) {
+        if (isAndroidNPlus(caller) && wouldToggleZenMode(ringerMode)
+                && !mNm.isNotificationPolicyAccessGrantedForPackage(caller)) {
+            throw new SecurityException("Not allowed to change Do Not Disturb state");
+        }
+
+        setRingerMode(ringerMode, caller, true /*external*/);
+    }
+
+    public void setRingerModeInternal(int ringerMode, String caller) {
+        enforceVolumeController("setRingerModeInternal");
+        setRingerMode(ringerMode, caller, false /*external*/);
+    }
+
+    public void silenceRingerModeInternal(String reason) {
+        VibrationEffect effect = null;
+        int ringerMode = AudioManager.RINGER_MODE_SILENT;
+        int toastText = 0;
+
+        int silenceRingerSetting = Settings.Secure.VOLUME_HUSH_OFF;
+        if (mContext.getResources()
+                .getBoolean(com.android.internal.R.bool.config_volumeHushGestureEnabled)) {
+            silenceRingerSetting = Settings.Secure.getIntForUser(mContentResolver,
+                    Settings.Secure.VOLUME_HUSH_GESTURE, VOLUME_HUSH_OFF,
+                    UserHandle.USER_CURRENT);
+        }
+
+        switch(silenceRingerSetting) {
+            case VOLUME_HUSH_MUTE:
+                effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
+                ringerMode = AudioManager.RINGER_MODE_SILENT;
+                toastText = com.android.internal.R.string.volume_dialog_ringer_guidance_silent;
+                break;
+            case VOLUME_HUSH_VIBRATE:
+                effect = VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+                ringerMode = AudioManager.RINGER_MODE_VIBRATE;
+                toastText = com.android.internal.R.string.volume_dialog_ringer_guidance_vibrate;
+                break;
+        }
+        maybeVibrate(effect, reason);
+        setRingerModeInternal(ringerMode, reason);
+        Toast.makeText(mContext, toastText, Toast.LENGTH_SHORT).show();
+    }
+
+    private boolean maybeVibrate(VibrationEffect effect, String reason) {
+        if (!mHasVibrator) {
+            return false;
+        }
+        final boolean hapticsDisabled = Settings.System.getIntForUser(mContext.getContentResolver(),
+                Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0;
+        if (hapticsDisabled) {
+            return false;
+        }
+
+        if (effect == null) {
+            return false;
+        }
+        mVibrator.vibrate(Binder.getCallingUid(), mContext.getOpPackageName(), effect,
+                reason, VIBRATION_ATTRIBUTES);
+        return true;
+    }
+
+    private void setRingerMode(int ringerMode, String caller, boolean external) {
+        if (mUseFixedVolume || mIsSingleVolume) {
+            return;
+        }
+        if (caller == null || caller.length() == 0) {
+            throw new IllegalArgumentException("Bad caller: " + caller);
+        }
+        ensureValidRingerMode(ringerMode);
+        if ((ringerMode == AudioManager.RINGER_MODE_VIBRATE) && !mHasVibrator) {
+            ringerMode = AudioManager.RINGER_MODE_SILENT;
+        }
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            synchronized (mSettingsLock) {
+                final int ringerModeInternal = getRingerModeInternal();
+                final int ringerModeExternal = getRingerModeExternal();
+                if (external) {
+                    setRingerModeExt(ringerMode);
+                    if (mRingerModeDelegate != null) {
+                        ringerMode = mRingerModeDelegate.onSetRingerModeExternal(ringerModeExternal,
+                                ringerMode, caller, ringerModeInternal, mVolumePolicy);
+                    }
+                    if (ringerMode != ringerModeInternal) {
+                        setRingerModeInt(ringerMode, true /*persist*/);
+                    }
+                } else /*internal*/ {
+                    if (ringerMode != ringerModeInternal) {
+                        setRingerModeInt(ringerMode, true /*persist*/);
+                    }
+                    if (mRingerModeDelegate != null) {
+                        ringerMode = mRingerModeDelegate.onSetRingerModeInternal(ringerModeInternal,
+                                ringerMode, caller, ringerModeExternal, mVolumePolicy);
+                    }
+                    setRingerModeExt(ringerMode);
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private void setRingerModeExt(int ringerMode) {
+        synchronized(mSettingsLock) {
+            if (ringerMode == mRingerModeExternal) return;
+            mRingerModeExternal = ringerMode;
+        }
+        // Send sticky broadcast
+        broadcastRingerMode(AudioManager.RINGER_MODE_CHANGED_ACTION, ringerMode);
+    }
+
+    @GuardedBy("mSettingsLock")
+    private void muteRingerModeStreams() {
+        // Mute stream if not previously muted by ringer mode and (ringer mode
+        // is not RINGER_MODE_NORMAL OR stream is zen muted) and stream is affected by ringer mode.
+        // Unmute stream if previously muted by ringer/zen mode and ringer mode
+        // is RINGER_MODE_NORMAL or stream is not affected by ringer mode.
+        int numStreamTypes = AudioSystem.getNumStreamTypes();
+
+        if (mNm == null) {
+            mNm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        }
+
+        final int ringerMode = mRingerMode; // Read ringer mode as reading primitives is atomic
+        final boolean ringerModeMute = ringerMode == AudioManager.RINGER_MODE_VIBRATE
+                || ringerMode == AudioManager.RINGER_MODE_SILENT;
+        final boolean shouldRingSco = ringerMode == AudioManager.RINGER_MODE_VIBRATE
+                && isBluetoothScoOn();
+        // Ask audio policy engine to force use Bluetooth SCO channel if needed
+        final String eventSource = "muteRingerModeStreams() from u/pid:" + Binder.getCallingUid()
+                + "/" + Binder.getCallingPid();
+        sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE, AudioSystem.FOR_VIBRATE_RINGING,
+                shouldRingSco ? AudioSystem.FORCE_BT_SCO : AudioSystem.FORCE_NONE, eventSource, 0);
+
+        for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+            final boolean isMuted = isStreamMutedByRingerOrZenMode(streamType);
+            final boolean muteAllowedBySco =
+                    !(shouldRingSco && streamType == AudioSystem.STREAM_RING);
+            final boolean shouldZenMute = shouldZenMuteStream(streamType);
+            final boolean shouldMute = shouldZenMute || (ringerModeMute
+                    && isStreamAffectedByRingerMode(streamType) && muteAllowedBySco);
+            if (isMuted == shouldMute) continue;
+            if (!shouldMute) {
+                // unmute
+                // ring and notifications volume should never be 0 when not silenced
+                if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING) {
+                    synchronized (VolumeStreamState.class) {
+                        final VolumeStreamState vss = mStreamStates[streamType];
+                        for (int i = 0; i < vss.mIndexMap.size(); i++) {
+                            int device = vss.mIndexMap.keyAt(i);
+                            int value = vss.mIndexMap.valueAt(i);
+                            if (value == 0) {
+                                vss.setIndex(10, device, TAG);
+                            }
+                        }
+                        // Persist volume for stream ring when it is changed here
+                      final int device = getDeviceForStream(streamType);
+                      sendMsg(mAudioHandler,
+                              MSG_PERSIST_VOLUME,
+                              SENDMSG_QUEUE,
+                              device,
+                              0,
+                              mStreamStates[streamType],
+                              PERSIST_DELAY);
+                    }
+                }
+                mStreamStates[streamType].mute(false);
+                mRingerAndZenModeMutedStreams &= ~(1 << streamType);
+            } else {
+                // mute
+                mStreamStates[streamType].mute(true);
+                mRingerAndZenModeMutedStreams |= (1 << streamType);
+            }
+        }
+    }
+
+    private boolean isAlarm(int streamType) {
+        return streamType == AudioSystem.STREAM_ALARM;
+    }
+
+    private boolean isNotificationOrRinger(int streamType) {
+        return streamType == AudioSystem.STREAM_NOTIFICATION
+                || streamType == AudioSystem.STREAM_RING;
+    }
+
+    private boolean isMedia(int streamType) {
+        return streamType == AudioSystem.STREAM_MUSIC;
+    }
+
+
+    private boolean isSystem(int streamType) {
+        return streamType == AudioSystem.STREAM_SYSTEM;
+    }
+
+    private void setRingerModeInt(int ringerMode, boolean persist) {
+        final boolean change;
+        synchronized(mSettingsLock) {
+            change = mRingerMode != ringerMode;
+            mRingerMode = ringerMode;
+            muteRingerModeStreams();
+        }
+
+        // Post a persist ringer mode msg
+        if (persist) {
+            sendMsg(mAudioHandler, MSG_PERSIST_RINGER_MODE,
+                    SENDMSG_REPLACE, 0, 0, null, PERSIST_DELAY);
+        }
+        if (change) {
+            // Send sticky broadcast
+            broadcastRingerMode(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, ringerMode);
+        }
+    }
+
+    /*package*/ void postUpdateRingerModeServiceInt() {
+        sendMsg(mAudioHandler, MSG_UPDATE_RINGER_MODE, SENDMSG_QUEUE, 0, 0, null, 0);
+    }
+
+    private void onUpdateRingerModeServiceInt() {
+        setRingerModeInt(getRingerModeInternal(), false);
+    }
+
+    /** @see AudioManager#shouldVibrate(int) */
+    public boolean shouldVibrate(int vibrateType) {
+        if (!mHasVibrator) return false;
+
+        switch (getVibrateSetting(vibrateType)) {
+
+            case AudioManager.VIBRATE_SETTING_ON:
+                return getRingerModeExternal() != AudioManager.RINGER_MODE_SILENT;
+
+            case AudioManager.VIBRATE_SETTING_ONLY_SILENT:
+                return getRingerModeExternal() == AudioManager.RINGER_MODE_VIBRATE;
+
+            case AudioManager.VIBRATE_SETTING_OFF:
+                // return false, even for incoming calls
+                return false;
+
+            default:
+                return false;
+        }
+    }
+
+    /** @see AudioManager#getVibrateSetting(int) */
+    public int getVibrateSetting(int vibrateType) {
+        if (!mHasVibrator) return AudioManager.VIBRATE_SETTING_OFF;
+        return (mVibrateSetting >> (vibrateType * 2)) & 3;
+    }
+
+    /** @see AudioManager#setVibrateSetting(int, int) */
+    public void setVibrateSetting(int vibrateType, int vibrateSetting) {
+
+        if (!mHasVibrator) return;
+
+        mVibrateSetting = AudioSystem.getValueForVibrateSetting(mVibrateSetting, vibrateType,
+                vibrateSetting);
+
+        // Broadcast change
+        broadcastVibrateSetting(vibrateType);
+
+    }
+
+    /**
+     * Return the pid of the current audio mode owner
+     * @return 0 if nobody owns the mode
+     */
+    /*package*/ int getModeOwnerPid() {
+        int modeOwnerPid = 0;
+        try {
+            modeOwnerPid = mSetModeDeathHandlers.get(0).getPid();
+        } catch (Exception e) {
+            // nothing to do, modeOwnerPid is not modified
+        }
+        return modeOwnerPid;
+    }
+
+    /**
+     * Return the uid of the current audio mode owner
+     * @return 0 if nobody owns the mode
+     */
+    /*package*/ int getModeOwnerUid() {
+        int modeOwnerUid = 0;
+        try {
+            modeOwnerUid = mSetModeDeathHandlers.get(0).getUid();
+        } catch (Exception e) {
+            // nothing to do, modeOwnerUid is not modified
+        }
+        return modeOwnerUid;
+    }
+
+    private class SetModeDeathHandler implements IBinder.DeathRecipient {
+        private final IBinder mCb; // To be notified of client's death
+        private final int mPid;
+        private final int mUid;
+        private int mMode = AudioSystem.MODE_NORMAL; // Current mode set by this client
+
+        SetModeDeathHandler(IBinder cb, int pid, int uid) {
+            mCb = cb;
+            mPid = pid;
+            mUid = uid;
+        }
+
+        public void binderDied() {
+            int oldModeOwnerPid;
+            int newModeOwnerPid = 0;
+            synchronized (mDeviceBroker.mSetModeLock) {
+                Log.w(TAG, "setMode() client died");
+                oldModeOwnerPid = getModeOwnerPid();
+                int index = mSetModeDeathHandlers.indexOf(this);
+                if (index < 0) {
+                    Log.w(TAG, "unregistered setMode() client died");
+                } else {
+                    newModeOwnerPid = setModeInt(AudioSystem.MODE_NORMAL, mCb, mPid, mUid, TAG);
+                }
+            }
+            // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all
+            // SCO connections not started by the application changing the mode when pid changes
+            if ((newModeOwnerPid != oldModeOwnerPid) && (newModeOwnerPid != 0)) {
+                mDeviceBroker.postDisconnectBluetoothSco(newModeOwnerPid);
+            }
+        }
+
+        public int getPid() {
+            return mPid;
+        }
+
+        public void setMode(int mode) {
+            mMode = mode;
+        }
+
+        public int getMode() {
+            return mMode;
+        }
+
+        public IBinder getBinder() {
+            return mCb;
+        }
+
+        public int getUid() {
+            return mUid;
+        }
+    }
+
+    /** @see AudioManager#setMode(int) */
+    public void setMode(int mode, IBinder cb, String callingPackage) {
+        if (DEBUG_MODE) {
+            Log.v(TAG, "setMode(mode=" + mode + ", callingPackage=" + callingPackage + ")");
+        }
+        if (!checkAudioSettingsPermission("setMode()")) {
+            return;
+        }
+        final boolean hasModifyPhoneStatePermission = mContext.checkCallingOrSelfPermission(
+                        android.Manifest.permission.MODIFY_PHONE_STATE)
+                        == PackageManager.PERMISSION_GRANTED;
+        final int callingPid = Binder.getCallingPid();
+        if ((mode == AudioSystem.MODE_IN_CALL) && !hasModifyPhoneStatePermission) {
+            Log.w(TAG, "MODIFY_PHONE_STATE Permission Denial: setMode(MODE_IN_CALL) from pid="
+                    + callingPid + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+
+        if (mode == AudioSystem.MODE_CALL_SCREENING && !mIsCallScreeningModeSupported) {
+            Log.w(TAG, "setMode(MODE_CALL_SCREENING) not permitted "
+                    + "when call screening is not supported");
+            return;
+        }
+
+        if (mode < AudioSystem.MODE_CURRENT || mode >= AudioSystem.NUM_MODES) {
+            return;
+        }
+
+        int oldModeOwnerPid;
+        int newModeOwnerPid;
+        synchronized (mDeviceBroker.mSetModeLock) {
+            if (mode == AudioSystem.MODE_CURRENT) {
+                mode = mMode;
+            }
+            oldModeOwnerPid = getModeOwnerPid();
+            // Do not allow changing mode if a call is active and the requester
+            // does not have permission to modify phone state or is not the mode owner.
+            if (((mMode == AudioSystem.MODE_IN_CALL)
+                    || (mMode == AudioSystem.MODE_IN_COMMUNICATION))
+                    && !(hasModifyPhoneStatePermission || (oldModeOwnerPid == callingPid))) {
+                Log.w(TAG, "setMode(" + mode + ") from pid=" + callingPid
+                        + ", uid=" + Binder.getCallingUid()
+                        + ", cannot change mode from " + mMode
+                        + " without permission or being mode owner");
+                return;
+            }
+            newModeOwnerPid = setModeInt(
+                mode, cb, callingPid, Binder.getCallingUid(), callingPackage);
+        }
+        // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all
+        // SCO connections not started by the application changing the mode when pid changes
+        if ((newModeOwnerPid != oldModeOwnerPid) && (newModeOwnerPid != 0)) {
+            mDeviceBroker.postDisconnectBluetoothSco(newModeOwnerPid);
+        }
+    }
+
+    // setModeInt() returns a valid PID if the audio mode was successfully set to
+    // any mode other than NORMAL.
+    @GuardedBy("mDeviceBroker.mSetModeLock")
+    private int setModeInt(int mode, IBinder cb, int pid, int uid, String caller) {
+        if (DEBUG_MODE) {
+            Log.v(TAG, "setModeInt(mode=" + mode + ", pid=" + pid
+                    + ", uid=" + uid + ", caller=" + caller + ")");
+        }
+        int newModeOwnerPid = 0;
+        if (cb == null) {
+            Log.e(TAG, "setModeInt() called with null binder");
+            return newModeOwnerPid;
+        }
+
+        SetModeDeathHandler hdlr = null;
+        Iterator iter = mSetModeDeathHandlers.iterator();
+        while (iter.hasNext()) {
+            SetModeDeathHandler h = (SetModeDeathHandler)iter.next();
+            if (h.getPid() == pid) {
+                hdlr = h;
+                // Remove from client list so that it is re-inserted at top of list
+                iter.remove();
+                try {
+                    hdlr.getBinder().unlinkToDeath(hdlr, 0);
+                    if (cb != hdlr.getBinder()) {
+                        hdlr = null;
+                    }
+                } catch (NoSuchElementException e) {
+                    hdlr = null;
+                    Log.w(TAG, "link does not exist ...");
+                }
+                break;
+            }
+        }
+        final int oldMode = mMode;
+        int status = AudioSystem.AUDIO_STATUS_OK;
+        int actualMode;
+        do {
+            actualMode = mode;
+            if (mode == AudioSystem.MODE_NORMAL) {
+                // get new mode from client at top the list if any
+                if (!mSetModeDeathHandlers.isEmpty()) {
+                    hdlr = mSetModeDeathHandlers.get(0);
+                    cb = hdlr.getBinder();
+                    actualMode = hdlr.getMode();
+                    if (DEBUG_MODE) {
+                        Log.w(TAG, " using mode=" + mode + " instead due to death hdlr at pid="
+                                + hdlr.mPid);
+                    }
+                }
+            } else {
+                if (hdlr == null) {
+                    hdlr = new SetModeDeathHandler(cb, pid, uid);
+                }
+                // Register for client death notification
+                try {
+                    cb.linkToDeath(hdlr, 0);
+                } catch (RemoteException e) {
+                    // Client has died!
+                    Log.w(TAG, "setMode() could not link to "+cb+" binder death");
+                }
+
+                // Last client to call setMode() is always at top of client list
+                // as required by SetModeDeathHandler.binderDied()
+                mSetModeDeathHandlers.add(0, hdlr);
+                hdlr.setMode(mode);
+            }
+
+            if (actualMode != mMode) {
+                final long identity = Binder.clearCallingIdentity();
+                status = AudioSystem.setPhoneState(actualMode, getModeOwnerUid());
+                Binder.restoreCallingIdentity(identity);
+                if (status == AudioSystem.AUDIO_STATUS_OK) {
+                    if (DEBUG_MODE) { Log.v(TAG, " mode successfully set to " + actualMode); }
+                    mMode = actualMode;
+                } else {
+                    if (hdlr != null) {
+                        mSetModeDeathHandlers.remove(hdlr);
+                        cb.unlinkToDeath(hdlr, 0);
+                    }
+                    // force reading new top of mSetModeDeathHandlers stack
+                    if (DEBUG_MODE) { Log.w(TAG, " mode set to MODE_NORMAL after phoneState pb"); }
+                    mode = AudioSystem.MODE_NORMAL;
+                }
+            } else {
+                status = AudioSystem.AUDIO_STATUS_OK;
+            }
+        } while (status != AudioSystem.AUDIO_STATUS_OK && !mSetModeDeathHandlers.isEmpty());
+
+        if (status == AudioSystem.AUDIO_STATUS_OK) {
+            if (actualMode != AudioSystem.MODE_NORMAL) {
+                newModeOwnerPid = getModeOwnerPid();
+                if (newModeOwnerPid == 0) {
+                    Log.e(TAG, "setMode() different from MODE_NORMAL with empty mode client stack");
+                }
+            }
+            // Note: newModeOwnerPid is always 0 when actualMode is MODE_NORMAL
+            mModeLogger.log(
+                    new PhoneStateEvent(caller, pid, mode, newModeOwnerPid, actualMode));
+            int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE);
+            int device = getDeviceForStream(streamType);
+            int index = mStreamStates[mStreamVolumeAlias[streamType]].getIndex(device);
+            setStreamVolumeInt(mStreamVolumeAlias[streamType], index, device, true, caller);
+
+            updateStreamVolumeAlias(true /*updateVolumes*/, caller);
+
+            // change of mode may require volume to be re-applied on some devices
+            updateAbsVolumeMultiModeDevices(oldMode, actualMode);
+        }
+        return newModeOwnerPid;
+    }
+
+    /** @see AudioManager#getMode() */
+    public int getMode() {
+        return mMode;
+    }
+
+    /** cached value read from audiopolicy manager after initialization. */
+    private boolean mIsCallScreeningModeSupported = false;
+
+    /** @see AudioManager#isCallScreeningModeSupported() */
+    public boolean isCallScreeningModeSupported() {
+        return mIsCallScreeningModeSupported;
+    }
+
+    /** @see AudioManager#setRttEnabled() */
+    @Override
+    public void setRttEnabled(boolean rttEnabled) {
+        if (mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE)
+                != PackageManager.PERMISSION_GRANTED) {
+            Log.w(TAG, "MODIFY_PHONE_STATE Permission Denial: setRttEnabled from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+        synchronized (mSettingsLock) {
+            mRttEnabled = rttEnabled;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                AudioSystem.setRttEnabled(rttEnabled);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    //==========================================================================================
+    // Sound Effects
+    //==========================================================================================
+    private static final class LoadSoundEffectReply
+            implements SoundEffectsHelper.OnEffectsLoadCompleteHandler {
+        private static final int SOUND_EFFECTS_LOADING = 1;
+        private static final int SOUND_EFFECTS_LOADED = 0;
+        private static final int SOUND_EFFECTS_ERROR = -1;
+        private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 5000;
+
+        private int mStatus = SOUND_EFFECTS_LOADING;
+
+        @Override
+        public synchronized void run(boolean success) {
+            mStatus = success ? SOUND_EFFECTS_LOADED : SOUND_EFFECTS_ERROR;
+            notify();
+        }
+
+        public synchronized boolean waitForLoaded(int attempts) {
+            while ((mStatus == SOUND_EFFECTS_LOADING) && (attempts-- > 0)) {
+                try {
+                    wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Interrupted while waiting sound pool loaded.");
+                }
+            }
+            return mStatus == SOUND_EFFECTS_LOADED;
+        }
+    }
+
+    /** @see AudioManager#playSoundEffect(int) */
+    public void playSoundEffect(int effectType) {
+        playSoundEffectVolume(effectType, -1.0f);
+    }
+
+    /** @see AudioManager#playSoundEffect(int, float) */
+    public void playSoundEffectVolume(int effectType, float volume) {
+        // do not try to play the sound effect if the system stream is muted
+        if (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) {
+            return;
+        }
+
+        if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {
+            Log.w(TAG, "AudioService effectType value " + effectType + " out of range");
+            return;
+        }
+
+        sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_QUEUE,
+                effectType, (int) (volume * 1000), null, 0);
+    }
+
+    /**
+     * Loads samples into the soundpool.
+     * This method must be called at first when sound effects are enabled
+     */
+    public boolean loadSoundEffects() {
+        LoadSoundEffectReply reply = new LoadSoundEffectReply();
+        sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0);
+        return reply.waitForLoaded(3 /*attempts*/);
+    }
+
+    /**
+     * Schedule loading samples into the soundpool.
+     * This method can be overridden to schedule loading at a later time.
+     */
+    protected void scheduleLoadSoundEffects() {
+        sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0);
+    }
+
+    /**
+     *  Unloads samples from the sound pool.
+     *  This method can be called to free some memory when
+     *  sound effects are disabled.
+     */
+    public void unloadSoundEffects() {
+        sendMsg(mAudioHandler, MSG_UNLOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0);
+    }
+
+    /** @see AudioManager#reloadAudioSettings() */
+    public void reloadAudioSettings() {
+        readAudioSettings(false /*userSwitch*/);
+    }
+
+    private void readAudioSettings(boolean userSwitch) {
+        // restore ringer mode, ringer mode affected streams, mute affected streams and vibrate settings
+        readPersistedSettings();
+        readUserRestrictions();
+
+        // restore volume settings
+        int numStreamTypes = AudioSystem.getNumStreamTypes();
+        for (int streamType = 0; streamType < numStreamTypes; streamType++) {
+            VolumeStreamState streamState = mStreamStates[streamType];
+
+            if (userSwitch && mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) {
+                continue;
+            }
+
+            streamState.readSettings();
+            synchronized (VolumeStreamState.class) {
+                // unmute stream that was muted but is not affect by mute anymore
+                if (streamState.mIsMuted && ((!isStreamAffectedByMute(streamType) &&
+                        !isStreamMutedByRingerOrZenMode(streamType)) || mUseFixedVolume)) {
+                    streamState.mIsMuted = false;
+                }
+            }
+        }
+
+        // apply new ringer mode before checking volume for alias streams so that streams
+        // muted by ringer mode have the correct volume
+        setRingerModeInt(getRingerModeInternal(), false);
+
+        checkAllFixedVolumeDevices();
+        checkAllAliasStreamVolumes();
+        checkMuteAffectedStreams();
+
+        synchronized (mSafeMediaVolumeStateLock) {
+            mMusicActiveMs = MathUtils.constrain(Settings.Secure.getIntForUser(mContentResolver,
+                    Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, 0, UserHandle.USER_CURRENT),
+                    0, UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX);
+            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE) {
+                enforceSafeMediaVolume(TAG);
+            }
+        }
+
+        readVolumeGroupsSettings();
+    }
+
+    /** @see AudioManager#setSpeakerphoneOn(boolean) */
+    public void setSpeakerphoneOn(boolean on){
+        if (!checkAudioSettingsPermission("setSpeakerphoneOn()")) {
+            return;
+        }
+
+        if (mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE)
+                != PackageManager.PERMISSION_GRANTED) {
+            synchronized (mSetModeDeathHandlers) {
+                for (SetModeDeathHandler h : mSetModeDeathHandlers) {
+                    if (h.getMode() == AudioSystem.MODE_IN_CALL) {
+                        Log.w(TAG, "getMode is call, Permission Denial: setSpeakerphoneOn from pid="
+                                + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
+                        return;
+                    }
+                }
+            }
+        }
+
+        // for logging only
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final String eventSource = new StringBuilder("setSpeakerphoneOn(").append(on)
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
+        final boolean stateChanged = mDeviceBroker.setSpeakerphoneOn(on, eventSource);
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+                + MediaMetrics.SEPARATOR + "setSpeakerphoneOn")
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.STATE, on
+                        ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
+                .record();
+
+        if (stateChanged) {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                mContext.sendBroadcastAsUser(
+                        new Intent(AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED)
+                                .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY), UserHandle.ALL);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+    }
+
+    /** @see AudioManager#isSpeakerphoneOn() */
+    public boolean isSpeakerphoneOn() {
+        return mDeviceBroker.isSpeakerphoneOn();
+    }
+
+    /** @see AudioManager#setBluetoothScoOn(boolean) */
+    public void setBluetoothScoOn(boolean on) {
+        if (!checkAudioSettingsPermission("setBluetoothScoOn()")) {
+            return;
+        }
+
+        // Only enable calls from system components
+        if (UserHandle.getCallingAppId() >= FIRST_APPLICATION_UID) {
+            mDeviceBroker.setBluetoothScoOnByApp(on);
+            return;
+        }
+
+        // for logging only
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final String eventSource = new StringBuilder("setBluetoothScoOn(").append(on)
+                .append(") from u/pid:").append(uid).append("/").append(pid).toString();
+
+        //bt sco
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+                + MediaMetrics.SEPARATOR + "setBluetoothScoOn")
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.STATE, on
+                        ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
+                .record();
+
+        mDeviceBroker.setBluetoothScoOn(on, eventSource);
+    }
+
+    /** @see AudioManager#isBluetoothScoOn()
+     * Note that it doesn't report internal state, but state seen by apps (which may have
+     * called setBluetoothScoOn() */
+    public boolean isBluetoothScoOn() {
+        return mDeviceBroker.isBluetoothScoOnForApp();
+    }
+
+    // TODO investigate internal users due to deprecation of SDK API
+    /** @see AudioManager#setBluetoothA2dpOn(boolean) */
+    public void setBluetoothA2dpOn(boolean on) {
+        // for logging only
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on)
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
+
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+                + MediaMetrics.SEPARATOR + "setBluetoothA2dpOn")
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.STATE, on
+                        ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
+                .record();
+
+        mDeviceBroker.setBluetoothA2dpOn_Async(on, eventSource);
+    }
+
+    /** @see AudioManager#isBluetoothA2dpOn() */
+    public boolean isBluetoothA2dpOn() {
+        return mDeviceBroker.isBluetoothA2dpOn();
+    }
+
+    /** @see AudioManager#startBluetoothSco() */
+    public void startBluetoothSco(IBinder cb, int targetSdkVersion) {
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final int scoAudioMode =
+                (targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2) ?
+                        BtHelper.SCO_MODE_VIRTUAL_CALL : BtHelper.SCO_MODE_UNDEFINED;
+        final String eventSource = new StringBuilder("startBluetoothSco()")
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
+
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.EVENT, "startBluetoothSco")
+                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
+                        BtHelper.scoAudioModeToString(scoAudioMode))
+                .record();
+        startBluetoothScoInt(cb, scoAudioMode, eventSource);
+
+    }
+
+    /** @see AudioManager#startBluetoothScoVirtualCall() */
+    public void startBluetoothScoVirtualCall(IBinder cb) {
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final String eventSource = new StringBuilder("startBluetoothScoVirtualCall()")
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
+
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.EVENT, "startBluetoothScoVirtualCall")
+                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
+                        BtHelper.scoAudioModeToString(BtHelper.SCO_MODE_VIRTUAL_CALL))
+                .record();
+        startBluetoothScoInt(cb, BtHelper.SCO_MODE_VIRTUAL_CALL, eventSource);
+    }
+
+    void startBluetoothScoInt(IBinder cb, int scoAudioMode, @NonNull String eventSource) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
+                .set(MediaMetrics.Property.EVENT, "startBluetoothScoInt")
+                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
+                        BtHelper.scoAudioModeToString(scoAudioMode));
+
+        if (!checkAudioSettingsPermission("startBluetoothSco()") ||
+                !mSystemReady) {
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "permission or systemReady").record();
+            return;
+        }
+        synchronized (mDeviceBroker.mSetModeLock) {
+            mDeviceBroker.startBluetoothScoForClient_Sync(cb, scoAudioMode, eventSource);
+        }
+        mmi.record();
+    }
+
+    /** @see AudioManager#stopBluetoothSco() */
+    public void stopBluetoothSco(IBinder cb){
+        if (!checkAudioSettingsPermission("stopBluetoothSco()") ||
+                !mSystemReady) {
+            return;
+        }
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final String eventSource =  new StringBuilder("stopBluetoothSco()")
+                .append(") from u/pid:").append(uid).append("/")
+                .append(pid).toString();
+        synchronized (mDeviceBroker.mSetModeLock) {
+            mDeviceBroker.stopBluetoothScoForClient_Sync(cb, eventSource);
+        }
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
+                .setUid(uid)
+                .setPid(pid)
+                .set(MediaMetrics.Property.EVENT, "stopBluetoothSco")
+                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
+                        BtHelper.scoAudioModeToString(BtHelper.SCO_MODE_UNDEFINED))
+                .record();
+    }
+
+
+    /*package*/ ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    private void onCheckMusicActive(String caller) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) {
+                int device = getDeviceForStream(AudioSystem.STREAM_MUSIC);
+
+                if (mSafeMediaVolumeDevices.contains(device)) {
+                    sendMsg(mAudioHandler,
+                            MSG_CHECK_MUSIC_ACTIVE,
+                            SENDMSG_REPLACE,
+                            0,
+                            0,
+                            caller,
+                            MUSIC_ACTIVE_POLL_PERIOD_MS);
+                    int index = mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(device);
+                    if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)
+                            && (index > safeMediaVolumeIndex(device))) {
+                        // Approximate cumulative active music time
+                        mMusicActiveMs += MUSIC_ACTIVE_POLL_PERIOD_MS;
+                        if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
+                            setSafeMediaVolumeEnabled(true, caller);
+                            mMusicActiveMs = 0;
+                        }
+                        saveMusicActiveMs();
+                    }
+                }
+            }
+        }
+    }
+
+    private void saveMusicActiveMs() {
+        mAudioHandler.obtainMessage(MSG_PERSIST_MUSIC_ACTIVE_MS, mMusicActiveMs, 0).sendToTarget();
+    }
+
+    private int getSafeUsbMediaVolumeIndex() {
+        // determine UI volume index corresponding to the wanted safe gain in dBFS
+        int min = MIN_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
+        int max = MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
+
+        mSafeUsbMediaVolumeDbfs = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_safe_media_volume_usb_mB) / 100.0f;
+
+        while (Math.abs(max - min) > 1) {
+            int index = (max + min) / 2;
+            float gainDB = AudioSystem.getStreamVolumeDB(
+                    AudioSystem.STREAM_MUSIC, index, AudioSystem.DEVICE_OUT_USB_HEADSET);
+            if (Float.isNaN(gainDB)) {
+                //keep last min in case of read error
+                break;
+            } else if (gainDB == mSafeUsbMediaVolumeDbfs) {
+                min = index;
+                break;
+            } else if (gainDB < mSafeUsbMediaVolumeDbfs) {
+                min = index;
+            } else {
+                max = index;
+            }
+        }
+        return min * 10;
+    }
+
+    private void onConfigureSafeVolume(boolean force, String caller) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            int mcc = mContext.getResources().getConfiguration().mcc;
+            if ((mMcc != mcc) || ((mMcc == 0) && force)) {
+                mSafeMediaVolumeIndex = mContext.getResources().getInteger(
+                        com.android.internal.R.integer.config_safe_media_volume_index) * 10;
+
+                mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
+
+                boolean safeMediaVolumeEnabled =
+                        SystemProperties.getBoolean("audio.safemedia.force", false)
+                        || mContext.getResources().getBoolean(
+                                com.android.internal.R.bool.config_safe_media_volume_enabled);
+
+                boolean safeMediaVolumeBypass =
+                        SystemProperties.getBoolean("audio.safemedia.bypass", false);
+
+                // The persisted state is either "disabled" or "active": this is the state applied
+                // next time we boot and cannot be "inactive"
+                int persistedState;
+                if (safeMediaVolumeEnabled && !safeMediaVolumeBypass) {
+                    persistedState = SAFE_MEDIA_VOLUME_ACTIVE;
+                    // The state can already be "inactive" here if the user has forced it before
+                    // the 30 seconds timeout for forced configuration. In this case we don't reset
+                    // it to "active".
+                    if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) {
+                        if (mMusicActiveMs == 0) {
+                            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
+                            enforceSafeMediaVolume(caller);
+                        } else {
+                            // We have existing playback time recorded, already confirmed.
+                            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
+                        }
+                    }
+                } else {
+                    persistedState = SAFE_MEDIA_VOLUME_DISABLED;
+                    mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
+                }
+                mMcc = mcc;
+                sendMsg(mAudioHandler,
+                        MSG_PERSIST_SAFE_VOLUME_STATE,
+                        SENDMSG_QUEUE,
+                        persistedState,
+                        0,
+                        null,
+                        0);
+            }
+        }
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Internal methods
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Checks if the adjustment should change ringer mode instead of just
+     * adjusting volume. If so, this will set the proper ringer mode and volume
+     * indices on the stream states.
+     */
+    private int checkForRingerModeChange(int oldIndex, int direction, int step, boolean isMuted,
+            String caller, int flags) {
+        int result = FLAG_ADJUST_VOLUME;
+        if (isPlatformTelevision() || mIsSingleVolume) {
+            return result;
+        }
+
+        int ringerMode = getRingerModeInternal();
+
+        switch (ringerMode) {
+        case RINGER_MODE_NORMAL:
+            if (direction == AudioManager.ADJUST_LOWER) {
+                if (mHasVibrator) {
+                    // "step" is the delta in internal index units corresponding to a
+                    // change of 1 in UI index units.
+                    // Because of rounding when rescaling from one stream index range to its alias
+                    // index range, we cannot simply test oldIndex == step:
+                    //   (step <= oldIndex < 2 * step) is equivalent to: (old UI index == 1)
+                    if (step <= oldIndex && oldIndex < 2 * step) {
+                        ringerMode = RINGER_MODE_VIBRATE;
+                        mLoweredFromNormalToVibrateTime = SystemClock.uptimeMillis();
+                    }
+                } else {
+                    if (oldIndex == step && mVolumePolicy.volumeDownToEnterSilent) {
+                        ringerMode = RINGER_MODE_SILENT;
+                    }
+                }
+            } else if (mIsSingleVolume && (direction == AudioManager.ADJUST_TOGGLE_MUTE
+                    || direction == AudioManager.ADJUST_MUTE)) {
+                if (mHasVibrator) {
+                    ringerMode = RINGER_MODE_VIBRATE;
+                } else {
+                    ringerMode = RINGER_MODE_SILENT;
+                }
+                // Setting the ringer mode will toggle mute
+                result &= ~FLAG_ADJUST_VOLUME;
+            }
+            break;
+        case RINGER_MODE_VIBRATE:
+            if (!mHasVibrator) {
+                Log.e(TAG, "checkForRingerModeChange() current ringer mode is vibrate" +
+                        "but no vibrator is present");
+                break;
+            }
+            if ((direction == AudioManager.ADJUST_LOWER)) {
+                // This is the case we were muted with the volume turned up
+                if (mIsSingleVolume && oldIndex >= 2 * step && isMuted) {
+                    ringerMode = RINGER_MODE_NORMAL;
+                } else if (mPrevVolDirection != AudioManager.ADJUST_LOWER) {
+                    if (mVolumePolicy.volumeDownToEnterSilent) {
+                        final long diff = SystemClock.uptimeMillis()
+                                - mLoweredFromNormalToVibrateTime;
+                        if (diff > mVolumePolicy.vibrateToSilentDebounce
+                                && mRingerModeDelegate.canVolumeDownEnterSilent()) {
+                            ringerMode = RINGER_MODE_SILENT;
+                        }
+                    } else {
+                        result |= AudioManager.FLAG_SHOW_VIBRATE_HINT;
+                    }
+                }
+            } else if (direction == AudioManager.ADJUST_RAISE
+                    || direction == AudioManager.ADJUST_TOGGLE_MUTE
+                    || direction == AudioManager.ADJUST_UNMUTE) {
+                ringerMode = RINGER_MODE_NORMAL;
+            }
+            result &= ~FLAG_ADJUST_VOLUME;
+            break;
+        case RINGER_MODE_SILENT:
+            if (mIsSingleVolume && direction == AudioManager.ADJUST_LOWER && oldIndex >= 2 * step && isMuted) {
+                // This is the case we were muted with the volume turned up
+                ringerMode = RINGER_MODE_NORMAL;
+            } else if (direction == AudioManager.ADJUST_RAISE
+                    || direction == AudioManager.ADJUST_TOGGLE_MUTE
+                    || direction == AudioManager.ADJUST_UNMUTE) {
+                if (!mVolumePolicy.volumeUpToExitSilent) {
+                    result |= AudioManager.FLAG_SHOW_SILENT_HINT;
+                } else {
+                  if (mHasVibrator && direction == AudioManager.ADJUST_RAISE) {
+                      ringerMode = RINGER_MODE_VIBRATE;
+                  } else {
+                      // If we don't have a vibrator or they were toggling mute
+                      // go straight back to normal.
+                      ringerMode = RINGER_MODE_NORMAL;
+                  }
+                }
+            }
+            result &= ~FLAG_ADJUST_VOLUME;
+            break;
+        default:
+            Log.e(TAG, "checkForRingerModeChange() wrong ringer mode: "+ringerMode);
+            break;
+        }
+
+        if (isAndroidNPlus(caller) && wouldToggleZenMode(ringerMode)
+                && !mNm.isNotificationPolicyAccessGrantedForPackage(caller)
+                && (flags & AudioManager.FLAG_FROM_KEY) == 0) {
+            throw new SecurityException("Not allowed to change Do Not Disturb state");
+        }
+
+        setRingerMode(ringerMode, TAG + ".checkForRingerModeChange", false /*external*/);
+
+        mPrevVolDirection = direction;
+
+        return result;
+    }
+
+    @Override
+    public boolean isStreamAffectedByRingerMode(int streamType) {
+        return (mRingerModeAffectedStreams & (1 << streamType)) != 0;
+    }
+
+    private boolean shouldZenMuteStream(int streamType) {
+        if (mNm.getZenMode() != Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
+            return false;
+        }
+
+        NotificationManager.Policy zenPolicy = mNm.getConsolidatedNotificationPolicy();
+        final boolean muteAlarms = (zenPolicy.priorityCategories
+                & NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS) == 0;
+        final boolean muteMedia = (zenPolicy.priorityCategories
+                & NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA) == 0;
+        final boolean muteSystem = (zenPolicy.priorityCategories
+                & NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM) == 0;
+        final boolean muteNotificationAndRing = ZenModeConfig
+                .areAllPriorityOnlyRingerSoundsMuted(zenPolicy);
+        return muteAlarms && isAlarm(streamType)
+                || muteMedia && isMedia(streamType)
+                || muteSystem && isSystem(streamType)
+                || muteNotificationAndRing && isNotificationOrRinger(streamType);
+    }
+
+    private boolean isStreamMutedByRingerOrZenMode(int streamType) {
+        return (mRingerAndZenModeMutedStreams & (1 << streamType)) != 0;
+    }
+
+    /**
+     * Notifications, ringer and system sounds are controlled by the ringer:
+     * {@link ZenModeHelper.RingerModeDelegate#getRingerModeAffectedStreams(int)} but can
+     * also be muted by DND based on the DND mode:
+     * DND total silence: media and alarms streams can be muted by DND
+     * DND alarms only: no streams additionally controlled by DND
+     * DND priority only: alarms, media, system, ringer and notification streams can be muted by
+     * DND.  The current applied zenPolicy determines which streams will be muted by DND.
+     * @return true if changed, else false
+     */
+    private boolean updateZenModeAffectedStreams() {
+        if (!mSystemReady) {
+            return false;
+        }
+
+        int zenModeAffectedStreams = 0;
+        final int zenMode = mNm.getZenMode();
+
+        if (zenMode == Settings.Global.ZEN_MODE_NO_INTERRUPTIONS) {
+            zenModeAffectedStreams |= 1 << AudioManager.STREAM_ALARM;
+            zenModeAffectedStreams |= 1 << AudioManager.STREAM_MUSIC;
+        } else if (zenMode == Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
+            NotificationManager.Policy zenPolicy = mNm.getConsolidatedNotificationPolicy();
+            if ((zenPolicy.priorityCategories
+                    & NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS) == 0) {
+                zenModeAffectedStreams |= 1 << AudioManager.STREAM_ALARM;
+            }
+
+            if ((zenPolicy.priorityCategories
+                    & NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA) == 0) {
+                zenModeAffectedStreams |= 1 << AudioManager.STREAM_MUSIC;
+            }
+
+            // even if zen isn't muting the system stream, the ringer mode can still mute
+            // the system stream
+            if ((zenPolicy.priorityCategories
+                    & NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM) == 0) {
+                zenModeAffectedStreams |= 1 << AudioManager.STREAM_SYSTEM;
+            }
+
+            if (ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(zenPolicy)) {
+                zenModeAffectedStreams |= 1 << AudioManager.STREAM_NOTIFICATION;
+                zenModeAffectedStreams |= 1 << AudioManager.STREAM_RING;
+            }
+        }
+
+        if (mZenModeAffectedStreams != zenModeAffectedStreams) {
+            mZenModeAffectedStreams = zenModeAffectedStreams;
+            return true;
+        }
+
+        return false;
+    }
+
+    @GuardedBy("mSettingsLock")
+    private boolean updateRingerAndZenModeAffectedStreams() {
+        boolean updatedZenModeAffectedStreams = updateZenModeAffectedStreams();
+        int ringerModeAffectedStreams = Settings.System.getIntForUser(mContentResolver,
+                Settings.System.MODE_RINGER_STREAMS_AFFECTED,
+                ((1 << AudioSystem.STREAM_RING)|(1 << AudioSystem.STREAM_NOTIFICATION)|
+                 (1 << AudioSystem.STREAM_SYSTEM)|(1 << AudioSystem.STREAM_SYSTEM_ENFORCED)),
+                 UserHandle.USER_CURRENT);
+
+        if (mIsSingleVolume) {
+            ringerModeAffectedStreams = 0;
+        } else if (mRingerModeDelegate != null) {
+            ringerModeAffectedStreams = mRingerModeDelegate
+                    .getRingerModeAffectedStreams(ringerModeAffectedStreams);
+        }
+        if (mCameraSoundForced) {
+            ringerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
+        } else {
+            ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
+        }
+        if (mStreamVolumeAlias[AudioSystem.STREAM_DTMF] == AudioSystem.STREAM_RING) {
+            ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_DTMF);
+        } else {
+            ringerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_DTMF);
+        }
+
+        if (ringerModeAffectedStreams != mRingerModeAffectedStreams) {
+            Settings.System.putIntForUser(mContentResolver,
+                    Settings.System.MODE_RINGER_STREAMS_AFFECTED,
+                    ringerModeAffectedStreams,
+                    UserHandle.USER_CURRENT);
+            mRingerModeAffectedStreams = ringerModeAffectedStreams;
+            return true;
+        }
+        return updatedZenModeAffectedStreams;
+    }
+
+    @Override
+    public boolean isStreamAffectedByMute(int streamType) {
+        return (mMuteAffectedStreams & (1 << streamType)) != 0;
+    }
+
+    private void ensureValidDirection(int direction) {
+        switch (direction) {
+            case AudioManager.ADJUST_LOWER:
+            case AudioManager.ADJUST_RAISE:
+            case AudioManager.ADJUST_SAME:
+            case AudioManager.ADJUST_MUTE:
+            case AudioManager.ADJUST_UNMUTE:
+            case AudioManager.ADJUST_TOGGLE_MUTE:
+                break;
+            default:
+                throw new IllegalArgumentException("Bad direction " + direction);
+        }
+    }
+
+    private void ensureValidStreamType(int streamType) {
+        if (streamType < 0 || streamType >= mStreamStates.length) {
+            throw new IllegalArgumentException("Bad stream type " + streamType);
+        }
+    }
+
+    private boolean isMuteAdjust(int adjust) {
+        return adjust == AudioManager.ADJUST_MUTE || adjust == AudioManager.ADJUST_UNMUTE
+                || adjust == AudioManager.ADJUST_TOGGLE_MUTE;
+    }
+
+    /** only public for mocking/spying, do not call outside of AudioService */
+    @VisibleForTesting
+    public boolean isInCommunication() {
+        boolean IsInCall = false;
+
+        TelecomManager telecomManager =
+                (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
+
+        final long ident = Binder.clearCallingIdentity();
+        IsInCall = telecomManager.isInCall();
+        Binder.restoreCallingIdentity(ident);
+
+        return (IsInCall || getMode() == AudioManager.MODE_IN_COMMUNICATION ||
+                getMode() == AudioManager.MODE_IN_CALL);
+    }
+
+    /**
+     * For code clarity for getActiveStreamType(int)
+     * @param delay_ms max time since last stream activity to consider
+     * @return true if stream is active in streams handled by AudioFlinger now or
+     *     in the last "delay_ms" ms.
+     */
+    private boolean wasStreamActiveRecently(int stream, int delay_ms) {
+        return AudioSystem.isStreamActive(stream, delay_ms)
+                || AudioSystem.isStreamActiveRemotely(stream, delay_ms);
+    }
+
+    private int getActiveStreamType(int suggestedStreamType) {
+        if (mIsSingleVolume
+                && suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+            return AudioSystem.STREAM_MUSIC;
+        }
+
+        switch (mPlatformType) {
+        case AudioSystem.PLATFORM_VOICE:
+            if (isInCommunication()) {
+                if (AudioSystem.getForceUse(AudioSystem.FOR_COMMUNICATION)
+                        == AudioSystem.FORCE_BT_SCO) {
+                    // Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO...");
+                    return AudioSystem.STREAM_BLUETOOTH_SCO;
+                } else {
+                    // Log.v(TAG, "getActiveStreamType: Forcing STREAM_VOICE_CALL...");
+                    return AudioSystem.STREAM_VOICE_CALL;
+                }
+            } else if (suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+                if (wasStreamActiveRecently(AudioSystem.STREAM_RING, sStreamOverrideDelayMs)) {
+                    if (DEBUG_VOL)
+                        Log.v(TAG, "getActiveStreamType: Forcing STREAM_RING stream active");
+                    return AudioSystem.STREAM_RING;
+                } else if (wasStreamActiveRecently(
+                        AudioSystem.STREAM_NOTIFICATION, sStreamOverrideDelayMs)) {
+                    if (DEBUG_VOL)
+                        Log.v(TAG, "getActiveStreamType: Forcing STREAM_NOTIFICATION stream active");
+                    return AudioSystem.STREAM_NOTIFICATION;
+                } else {
+                    if (DEBUG_VOL) {
+                        Log.v(TAG, "getActiveStreamType: Forcing DEFAULT_VOL_STREAM_NO_PLAYBACK("
+                                + DEFAULT_VOL_STREAM_NO_PLAYBACK + ") b/c default");
+                    }
+                    return DEFAULT_VOL_STREAM_NO_PLAYBACK;
+                }
+            } else if (
+                    wasStreamActiveRecently(AudioSystem.STREAM_NOTIFICATION, sStreamOverrideDelayMs)) {
+                if (DEBUG_VOL)
+                    Log.v(TAG, "getActiveStreamType: Forcing STREAM_NOTIFICATION stream active");
+                return AudioSystem.STREAM_NOTIFICATION;
+            } else if (wasStreamActiveRecently(AudioSystem.STREAM_RING, sStreamOverrideDelayMs)) {
+                if (DEBUG_VOL)
+                    Log.v(TAG, "getActiveStreamType: Forcing STREAM_RING stream active");
+                return AudioSystem.STREAM_RING;
+            }
+        default:
+            if (isInCommunication()) {
+                if (AudioSystem.getForceUse(AudioSystem.FOR_COMMUNICATION)
+                        == AudioSystem.FORCE_BT_SCO) {
+                    if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO");
+                    return AudioSystem.STREAM_BLUETOOTH_SCO;
+                } else {
+                    if (DEBUG_VOL)  Log.v(TAG, "getActiveStreamType: Forcing STREAM_VOICE_CALL");
+                    return AudioSystem.STREAM_VOICE_CALL;
+                }
+            } else if (AudioSystem.isStreamActive(
+                    AudioSystem.STREAM_NOTIFICATION, sStreamOverrideDelayMs)) {
+                if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_NOTIFICATION");
+                return AudioSystem.STREAM_NOTIFICATION;
+            } else if (AudioSystem.isStreamActive(
+                    AudioSystem.STREAM_RING, sStreamOverrideDelayMs)) {
+                if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_RING");
+                return AudioSystem.STREAM_RING;
+            } else if (suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+                if (AudioSystem.isStreamActive(
+                        AudioSystem.STREAM_NOTIFICATION, sStreamOverrideDelayMs)) {
+                    if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_NOTIFICATION");
+                    return AudioSystem.STREAM_NOTIFICATION;
+                } else if (AudioSystem.isStreamActive(
+                        AudioSystem.STREAM_RING, sStreamOverrideDelayMs)) {
+                    if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_RING");
+                    return AudioSystem.STREAM_RING;
+                } else {
+                    if (DEBUG_VOL) {
+                        Log.v(TAG, "getActiveStreamType: Forcing DEFAULT_VOL_STREAM_NO_PLAYBACK("
+                                + DEFAULT_VOL_STREAM_NO_PLAYBACK + ") b/c default");
+                    }
+                    return DEFAULT_VOL_STREAM_NO_PLAYBACK;
+                }
+            }
+            break;
+        }
+        if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Returning suggested type "
+                + suggestedStreamType);
+        return suggestedStreamType;
+    }
+
+    private void broadcastRingerMode(String action, int ringerMode) {
+        if (!mSystemServer.isPrivileged()) {
+            return;
+        }
+        // Send sticky broadcast
+        Intent broadcast = new Intent(action);
+        broadcast.putExtra(AudioManager.EXTRA_RINGER_MODE, ringerMode);
+        broadcast.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+                | Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        sendStickyBroadcastToAll(broadcast);
+    }
+
+    private void broadcastVibrateSetting(int vibrateType) {
+        if (!mSystemServer.isPrivileged()) {
+            return;
+        }
+        // Send broadcast
+        if (mActivityManagerInternal.isSystemReady()) {
+            Intent broadcast = new Intent(AudioManager.VIBRATE_SETTING_CHANGED_ACTION);
+            broadcast.putExtra(AudioManager.EXTRA_VIBRATE_TYPE, vibrateType);
+            broadcast.putExtra(AudioManager.EXTRA_VIBRATE_SETTING, getVibrateSetting(vibrateType));
+            sendBroadcastToAll(broadcast);
+        }
+    }
+
+    // Message helper methods
+    /**
+     * Queue a message on the given handler's message queue, after acquiring the service wake lock.
+     * Note that the wake lock needs to be released after the message has been handled.
+     */
+    private void queueMsgUnderWakeLock(Handler handler, int msg,
+            int arg1, int arg2, Object obj, int delay) {
+        final long ident = Binder.clearCallingIdentity();
+        // Always acquire the wake lock as AudioService because it is released by the
+        // message handler.
+        mAudioEventWakeLock.acquire();
+        Binder.restoreCallingIdentity(ident);
+        sendMsg(handler, msg, SENDMSG_QUEUE, arg1, arg2, obj, delay);
+    }
+
+    private static void sendMsg(Handler handler, int msg,
+            int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) {
+        if (existingMsgPolicy == SENDMSG_REPLACE) {
+            handler.removeMessages(msg);
+        } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
+            return;
+        }
+
+        final long time = SystemClock.uptimeMillis() + delay;
+        handler.sendMessageAtTime(handler.obtainMessage(msg, arg1, arg2, obj), time);
+    }
+
+    boolean checkAudioSettingsPermission(String method) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS)
+                == PackageManager.PERMISSION_GRANTED) {
+            return true;
+        }
+        String msg = "Audio Settings Permission Denial: " + method + " from pid="
+                + Binder.getCallingPid()
+                + ", uid=" + Binder.getCallingUid();
+        Log.w(TAG, msg);
+        return false;
+    }
+
+    /** 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:
+            //  - speaker + one other device: give priority to speaker in this case.
+            //  - one A2DP device + another device: happens with duplicated output. In this case
+            // retain the device on the A2DP output as the other must not correspond to an active
+            // selection if not the speaker.
+            //  - HDMI-CEC system audio mode only output: give priority to available item in order.
+            // FIXME: Haven't applied audio device type refactor to this API
+            //  as it is going to be deprecated.
+            if ((device & AudioSystem.DEVICE_OUT_SPEAKER) != 0) {
+                device = AudioSystem.DEVICE_OUT_SPEAKER;
+            } else if ((device & AudioSystem.DEVICE_OUT_HDMI_ARC) != 0) {
+                device = AudioSystem.DEVICE_OUT_HDMI_ARC;
+            } else if ((device & AudioSystem.DEVICE_OUT_SPDIF) != 0) {
+                device = AudioSystem.DEVICE_OUT_SPDIF;
+            } else if ((device & AudioSystem.DEVICE_OUT_AUX_LINE) != 0) {
+                device = AudioSystem.DEVICE_OUT_AUX_LINE;
+            } else {
+                for (int deviceType : AudioSystem.DEVICE_OUT_ALL_A2DP_SET) {
+                    if ((deviceType & device) == deviceType) {
+                        return deviceType;
+                    }
+                }
+            }
+        }
+        return device;
+    }
+
+    private int getDevicesForStream(int stream) {
+        return getDevicesForStream(stream, true /*checkOthers*/);
+    }
+
+    private int getDevicesForStream(int stream, boolean checkOthers) {
+        ensureValidStreamType(stream);
+        synchronized (VolumeStreamState.class) {
+            return mStreamStates[stream].observeDevicesForStream_syncVSS(checkOthers);
+        }
+    }
+
+    private void observeDevicesForStreams(int skipStream) {
+        synchronized (VolumeStreamState.class) {
+            for (int stream = 0; stream < mStreamStates.length; stream++) {
+                if (stream != skipStream) {
+                    mStreamStates[stream].observeDevicesForStream_syncVSS(false /*checkOthers*/);
+                }
+            }
+        }
+    }
+
+    /** 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*/,
+                0 /*delay*/);
+    }
+
+    private void onObserveDevicesForAllStreams() {
+        observeDevicesForStreams(-1);
+    }
+
+    /**
+     * @see AudioManager#setDeviceVolumeBehavior(AudioDeviceAttributes, int)
+     * @param device the audio device to be affected
+     * @param deviceVolumeBehavior one of the device behaviors
+     */
+    public void setDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device,
+            @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior, @Nullable String pkgName) {
+        // verify permissions
+        enforceModifyAudioRoutingPermission();
+        // verify arguments
+        Objects.requireNonNull(device);
+        AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior);
+        if (pkgName == null) {
+            pkgName = "";
+        }
+        // translate Java device type to native device type (for the devices masks for full / fixed)
+        final int type;
+        switch (device.getType()) {
+            case AudioDeviceInfo.TYPE_HDMI:
+                type = AudioSystem.DEVICE_OUT_HDMI;
+                break;
+            case AudioDeviceInfo.TYPE_HDMI_ARC:
+                type = AudioSystem.DEVICE_OUT_HDMI_ARC;
+                break;
+            case AudioDeviceInfo.TYPE_LINE_DIGITAL:
+                type = AudioSystem.DEVICE_OUT_SPDIF;
+                break;
+            case AudioDeviceInfo.TYPE_AUX_LINE:
+                type = AudioSystem.DEVICE_OUT_LINE;
+                break;
+            default:
+                // unsupported for now
+                throw new IllegalArgumentException("Unsupported device type " + device.getType());
+        }
+        // update device masks based on volume behavior
+        switch (deviceVolumeBehavior) {
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE:
+                mFullVolumeDevices.remove(type);
+                mFixedVolumeDevices.remove(type);
+                break;
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_FIXED:
+                mFullVolumeDevices.remove(type);
+                mFixedVolumeDevices.add(type);
+                break;
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL:
+                mFullVolumeDevices.add(type);
+                mFixedVolumeDevices.remove(type);
+                break;
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE:
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE:
+                throw new IllegalArgumentException("Absolute volume unsupported for now");
+        }
+        // log event and caller
+        sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                "Volume behavior " + deviceVolumeBehavior
+                        + " for dev=0x" + Integer.toHexString(type) + " by pkg:" + pkgName));
+        // make sure we have a volume entry for this device, and that volume is updated according
+        // to volume behavior
+        checkAddAllFixedVolumeDevices(type, "setDeviceVolumeBehavior:" + pkgName);
+    }
+
+    /**
+     * @see AudioManager#getDeviceVolumeBehavior(AudioDeviceAttributes)
+     * @param device the audio output device type
+     * @return the volume behavior for the device
+     */
+    public @AudioManager.DeviceVolumeBehavior int getDeviceVolumeBehavior(
+            @NonNull AudioDeviceAttributes device) {
+        // verify permissions
+        enforceModifyAudioRoutingPermission();
+        // translate Java device type to native device type (for the devices masks for full / fixed)
+        final int type;
+        switch (device.getType()) {
+            case AudioDeviceInfo.TYPE_HEARING_AID:
+                type = AudioSystem.DEVICE_OUT_HEARING_AID;
+                break;
+            case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
+                type = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
+                break;
+            case AudioDeviceInfo.TYPE_HDMI:
+                type = AudioSystem.DEVICE_OUT_HDMI;
+                break;
+            case AudioDeviceInfo.TYPE_HDMI_ARC:
+                type = AudioSystem.DEVICE_OUT_HDMI_ARC;
+                break;
+            case AudioDeviceInfo.TYPE_LINE_DIGITAL:
+                type = AudioSystem.DEVICE_OUT_SPDIF;
+                break;
+            case AudioDeviceInfo.TYPE_AUX_LINE:
+                type = AudioSystem.DEVICE_OUT_LINE;
+                break;
+            default:
+                // unsupported for now
+                throw new IllegalArgumentException("Unsupported device type " + device.getType());
+        }
+        if ((mFullVolumeDevices.contains(type))) {
+            return AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL;
+        }
+        if ((mFixedVolumeDevices.contains(type))) {
+            return AudioManager.DEVICE_VOLUME_BEHAVIOR_FIXED;
+        }
+        if ((mAbsVolumeMultiModeCaseDevices.contains(type))) {
+            return AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE;
+        }
+        if (type == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP
+                && mDeviceBroker.isAvrcpAbsoluteVolumeSupported()) {
+            return AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
+        }
+        return AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE;
+    }
+
+    /*package*/ static final int CONNECTION_STATE_DISCONNECTED = 0;
+    /*package*/ static final int CONNECTION_STATE_CONNECTED = 1;
+    /**
+     * The states that can be used with AudioService.setWiredDeviceConnectionState()
+     * Attention: those values differ from those in BluetoothProfile, follow annotations to
+     * distinguish between @ConnectionState and @BtProfileConnectionState
+     */
+    @IntDef({
+            CONNECTION_STATE_DISCONNECTED,
+            CONNECTION_STATE_CONNECTED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ConnectionState {}
+
+    /**
+     * see AudioManager.setWiredDeviceConnectionState()
+     */
+    public void setWiredDeviceConnectionState(int type,
+            @ConnectionState int state, String address, String name,
+            String caller) {
+        enforceModifyAudioRoutingPermission();
+        if (state != CONNECTION_STATE_CONNECTED
+                && state != CONNECTION_STATE_DISCONNECTED) {
+            throw new IllegalArgumentException("Invalid state " + state);
+        }
+        new MediaMetrics.Item(mMetricsId + "setWiredDeviceConnectionState")
+                .set(MediaMetrics.Property.ADDRESS, address)
+                .set(MediaMetrics.Property.CLIENT_NAME, caller)
+                .set(MediaMetrics.Property.DEVICE, AudioSystem.getDeviceName(type))
+                .set(MediaMetrics.Property.NAME, name)
+                .set(MediaMetrics.Property.STATE,
+                        state == CONNECTION_STATE_CONNECTED ? "connected" : "disconnected")
+                .record();
+        mDeviceBroker.setWiredDeviceConnectionState(type, state, address, name, caller);
+    }
+
+    /**
+     * @hide
+     * The states that can be used with AudioService.setBluetoothHearingAidDeviceConnectionState()
+     * and AudioService.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent()
+     */
+    @IntDef({
+            BluetoothProfile.STATE_DISCONNECTED,
+            BluetoothProfile.STATE_CONNECTED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BtProfileConnectionState {}
+
+    /**
+     * See AudioManager.setBluetoothHearingAidDeviceConnectionState()
+     */
+    public void setBluetoothHearingAidDeviceConnectionState(
+            @NonNull BluetoothDevice device, @BtProfileConnectionState int state,
+            boolean suppressNoisyIntent, int musicDevice)
+    {
+        if (device == null) {
+            throw new IllegalArgumentException("Illegal null device");
+        }
+        if (state != BluetoothProfile.STATE_CONNECTED
+                && state != BluetoothProfile.STATE_DISCONNECTED) {
+            throw new IllegalArgumentException("Illegal BluetoothProfile state for device "
+                    + " (dis)connection, got " + state);
+        }
+        if (state == BluetoothProfile.STATE_CONNECTED) {
+            mPlaybackMonitor.registerPlaybackCallback(mVoiceActivityMonitor, true);
+        } else {
+            mPlaybackMonitor.unregisterPlaybackCallback(mVoiceActivityMonitor);
+        }
+        mDeviceBroker.postBluetoothHearingAidDeviceConnectionState(
+                device, state, suppressNoisyIntent, musicDevice, "AudioService");
+    }
+
+    /**
+     * See AudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent()
+     */
+    public void setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
+            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
+            int profile, boolean suppressNoisyIntent, int a2dpVolume) {
+        if (device == null) {
+            throw new IllegalArgumentException("Illegal null device");
+        }
+        if (state != BluetoothProfile.STATE_CONNECTED
+                && state != BluetoothProfile.STATE_DISCONNECTED) {
+            throw new IllegalArgumentException("Illegal BluetoothProfile state for device "
+                    + " (dis)connection, got " + state);
+        }
+        mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(device, state,
+                profile, suppressNoisyIntent, a2dpVolume);
+    }
+
+    /**
+     * See AudioManager.handleBluetoothA2dpDeviceConfigChange()
+     * @param device
+     */
+    public void handleBluetoothA2dpDeviceConfigChange(BluetoothDevice device)
+    {
+        if (device == null) {
+            throw new IllegalArgumentException("Illegal null device");
+        }
+        mDeviceBroker.postBluetoothA2dpDeviceConfigChange(device);
+    }
+
+    private static final Set<Integer> DEVICE_MEDIA_UNMUTED_ON_PLUG_SET;
+    static {
+        DEVICE_MEDIA_UNMUTED_ON_PLUG_SET = new HashSet<>();
+        DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADSET);
+        DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE);
+        DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_LINE);
+        DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+        DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET);
+        DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_HDMI);
+    }
+
+    /** 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);
+    }
+
+    private void onAccessoryPlugMediaUnmute(int newDevice) {
+        if (DEBUG_VOL) {
+            Log.i(TAG, String.format("onAccessoryPlugMediaUnmute newDevice=%d [%s]",
+                    newDevice, AudioSystem.getOutputDeviceName(newDevice)));
+        }
+
+        if (mNm.getZenMode() != Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
+                && !isStreamMutedByRingerOrZenMode(AudioSystem.STREAM_MUSIC)
+                && DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.contains(newDevice)
+                && mStreamStates[AudioSystem.STREAM_MUSIC].mIsMuted
+                && mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(newDevice) != 0
+                && (newDevice & AudioSystem.getDevicesForStream(AudioSystem.STREAM_MUSIC)) != 0) {
+            if (DEBUG_VOL) {
+                Log.i(TAG, String.format("onAccessoryPlugMediaUnmute unmuting device=%d [%s]",
+                        newDevice, AudioSystem.getOutputDeviceName(newDevice)));
+            }
+            mStreamStates[AudioSystem.STREAM_MUSIC].mute(false);
+        }
+    }
+
+    /**
+     * See AudioManager.hasHapticChannels(Uri).
+     */
+    public boolean hasHapticChannels(Uri uri) {
+        MediaExtractor extractor = new MediaExtractor();
+        try {
+            extractor.setDataSource(mContext, uri, null);
+            for (int i = 0; i < extractor.getTrackCount(); i++) {
+                MediaFormat format = extractor.getTrackFormat(i);
+                if (format.containsKey(MediaFormat.KEY_HAPTIC_CHANNEL_COUNT)
+                        && format.getInteger(MediaFormat.KEY_HAPTIC_CHANNEL_COUNT) > 0) {
+                    return true;
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "hasHapticChannels failure:" + e);
+        }
+        return false;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Inner classes
+    ///////////////////////////////////////////////////////////////////////////
+    /**
+     * Key is the AudioManager VolumeGroupId
+     * Value is the VolumeGroupState
+     */
+    private static final SparseArray<VolumeGroupState> sVolumeGroupStates = new SparseArray<>();
+
+    private void initVolumeGroupStates() {
+        for (final AudioVolumeGroup avg : getAudioVolumeGroups()) {
+            try {
+                // if no valid attributes, this volume group is not controllable, throw exception
+                ensureValidAttributes(avg);
+            } catch (IllegalArgumentException e) {
+                // Volume Groups without attributes are not controllable through set/get volume
+                // using attributes. Do not append them.
+                if (DEBUG_VOL) {
+                    Log.d(TAG, "volume group " + avg.name() + " for internal policy needs");
+                }
+                continue;
+            }
+            sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
+        }
+        for (int i = 0; i < sVolumeGroupStates.size(); i++) {
+            final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
+            vgs.applyAllVolumes();
+        }
+    }
+
+    private void ensureValidAttributes(AudioVolumeGroup avg) {
+        boolean hasAtLeastOneValidAudioAttributes = avg.getAudioAttributes().stream()
+                .anyMatch(aa -> !aa.equals(AudioProductStrategy.sDefaultAttributes));
+        if (!hasAtLeastOneValidAudioAttributes) {
+            throw new IllegalArgumentException("Volume Group " + avg.name()
+                    + " has no valid audio attributes");
+        }
+    }
+
+    private void readVolumeGroupsSettings() {
+        if (DEBUG_VOL) {
+            Log.v(TAG, "readVolumeGroupsSettings");
+        }
+        for (int i = 0; i < sVolumeGroupStates.size(); i++) {
+            final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
+            vgs.readSettings();
+            vgs.applyAllVolumes();
+        }
+    }
+
+    // Called upon crash of AudioServer
+    private void restoreVolumeGroups() {
+        if (DEBUG_VOL) {
+            Log.v(TAG, "restoreVolumeGroups");
+        }
+        for (int i = 0; i < sVolumeGroupStates.size(); i++) {
+            final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
+            vgs.applyAllVolumes();
+        }
+    }
+
+    private void dumpVolumeGroups(PrintWriter pw) {
+        pw.println("\nVolume Groups (device: index)");
+        for (int i = 0; i < sVolumeGroupStates.size(); i++) {
+            final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
+            vgs.dump(pw);
+            pw.println("");
+        }
+    }
+
+    // NOTE: Locking order for synchronized objects related to volume management:
+    //  1     mSettingsLock
+    //  2       VolumeGroupState.class
+    private class VolumeGroupState {
+        private final AudioVolumeGroup mAudioVolumeGroup;
+        private final SparseIntArray mIndexMap = new SparseIntArray(8);
+        private int mIndexMin;
+        private int mIndexMax;
+        private int mLegacyStreamType = AudioSystem.STREAM_DEFAULT;
+        private int mPublicStreamType = AudioSystem.STREAM_MUSIC;
+        private AudioAttributes mAudioAttributes = AudioProductStrategy.sDefaultAttributes;
+
+        // No API in AudioSystem to get a device from strategy or from attributes.
+        // Need a valid public stream type to use current API getDeviceForStream
+        private int getDeviceForVolume() {
+            return getDeviceForStream(mPublicStreamType);
+        }
+
+        private VolumeGroupState(AudioVolumeGroup avg) {
+            mAudioVolumeGroup = avg;
+            if (DEBUG_VOL) {
+                Log.v(TAG, "VolumeGroupState for " + avg.toString());
+            }
+            for (final AudioAttributes aa : avg.getAudioAttributes()) {
+                if (!aa.equals(AudioProductStrategy.sDefaultAttributes)) {
+                    mAudioAttributes = aa;
+                    break;
+                }
+            }
+            final int[] streamTypes = mAudioVolumeGroup.getLegacyStreamTypes();
+            if (streamTypes.length != 0) {
+                // Uses already initialized MIN / MAX if a stream type is attached to group
+                mLegacyStreamType = streamTypes[0];
+                for (final int streamType : streamTypes) {
+                    if (streamType != AudioSystem.STREAM_DEFAULT
+                            && streamType < AudioSystem.getNumStreamTypes()) {
+                        mPublicStreamType = streamType;
+                        break;
+                    }
+                }
+                mIndexMin = MIN_STREAM_VOLUME[mPublicStreamType];
+                mIndexMax = MAX_STREAM_VOLUME[mPublicStreamType];
+            } else if (!avg.getAudioAttributes().isEmpty()) {
+                mIndexMin = AudioSystem.getMinVolumeIndexForAttributes(mAudioAttributes);
+                mIndexMax = AudioSystem.getMaxVolumeIndexForAttributes(mAudioAttributes);
+            } else {
+                Log.e(TAG, "volume group: " + mAudioVolumeGroup.name()
+                        + " has neither valid attributes nor valid stream types assigned");
+                return;
+            }
+            // Load volume indexes from data base
+            readSettings();
+        }
+
+        public @NonNull int[] getLegacyStreamTypes() {
+            return mAudioVolumeGroup.getLegacyStreamTypes();
+        }
+
+        public String name() {
+            return mAudioVolumeGroup.name();
+        }
+
+        public int getVolumeIndex() {
+            return getIndex(getDeviceForVolume());
+        }
+
+        public void setVolumeIndex(int index, int flags) {
+            if (mUseFixedVolume) {
+                return;
+            }
+            setVolumeIndex(index, getDeviceForVolume(), flags);
+        }
+
+        private void setVolumeIndex(int index, int device, int flags) {
+            // Set the volume index
+            setVolumeIndexInt(index, device, flags);
+
+            // Update local cache
+            mIndexMap.put(device, index);
+
+            // update data base - post a persist volume group msg
+            sendMsg(mAudioHandler,
+                    MSG_PERSIST_VOLUME_GROUP,
+                    SENDMSG_QUEUE,
+                    device,
+                    0,
+                    this,
+                    PERSIST_DELAY);
+        }
+
+        private void setVolumeIndexInt(int index, int device, int flags) {
+            // Set the volume index
+            AudioSystem.setVolumeIndexForAttributes(mAudioAttributes, index, device);
+        }
+
+        public int getIndex(int device) {
+            synchronized (VolumeGroupState.class) {
+                int index = mIndexMap.get(device, -1);
+                // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
+                return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT);
+            }
+        }
+
+        public boolean hasIndexForDevice(int device) {
+            synchronized (VolumeGroupState.class) {
+                return (mIndexMap.get(device, -1) != -1);
+            }
+        }
+
+        public int getMaxIndex() {
+            return mIndexMax;
+        }
+
+        public int getMinIndex() {
+            return mIndexMin;
+        }
+
+        public void applyAllVolumes() {
+            synchronized (VolumeGroupState.class) {
+                // apply device specific volumes first
+                int index;
+                for (int i = 0; i < mIndexMap.size(); i++) {
+                    final int device = mIndexMap.keyAt(i);
+                    if (device != AudioSystem.DEVICE_OUT_DEFAULT) {
+                        index = mIndexMap.valueAt(i);
+                        if (DEBUG_VOL) {
+                            Log.v(TAG, "applyAllVolumes: restore index " + index + " for group "
+                                    + mAudioVolumeGroup.name() + " and device "
+                                    + AudioSystem.getOutputDeviceName(device));
+                        }
+                        setVolumeIndexInt(index, device, 0 /*flags*/);
+                    }
+                }
+                // apply default volume last: by convention , default device volume will be used
+                // by audio policy manager if no explicit volume is present for a given device type
+                index = getIndex(AudioSystem.DEVICE_OUT_DEFAULT);
+                if (DEBUG_VOL) {
+                    Log.v(TAG, "applyAllVolumes: restore default device index " + index
+                            + " for group " + mAudioVolumeGroup.name());
+                }
+                setVolumeIndexInt(index, AudioSystem.DEVICE_OUT_DEFAULT, 0 /*flags*/);
+            }
+        }
+
+        private void persistVolumeGroup(int device) {
+            if (mUseFixedVolume) {
+                return;
+            }
+            if (DEBUG_VOL) {
+                Log.v(TAG, "persistVolumeGroup: storing index " + getIndex(device) + " for group "
+                        + mAudioVolumeGroup.name() + " and device "
+                        + AudioSystem.getOutputDeviceName(device));
+            }
+            boolean success = Settings.System.putIntForUser(mContentResolver,
+                    getSettingNameForDevice(device),
+                    getIndex(device),
+                    UserHandle.USER_CURRENT);
+            if (!success) {
+                Log.e(TAG, "persistVolumeGroup failed for group " +  mAudioVolumeGroup.name());
+            }
+        }
+
+        public void readSettings() {
+            synchronized (VolumeGroupState.class) {
+                // First clear previously loaded (previous user?) settings
+                mIndexMap.clear();
+                // force maximum volume on all streams if fixed volume property is set
+                if (mUseFixedVolume) {
+                    mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax);
+                    return;
+                }
+                for (int device : AudioSystem.DEVICE_OUT_ALL_SET) {
+                    // retrieve current volume for device
+                    // if no volume stored for current volume group and device, use default volume
+                    // if default device, continue otherwise
+                    int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT)
+                            ? AudioSystem.DEFAULT_STREAM_VOLUME[mPublicStreamType] : -1;
+                    int index;
+                    String name = getSettingNameForDevice(device);
+                    index = Settings.System.getIntForUser(
+                            mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT);
+                    if (index == -1) {
+                        continue;
+                    }
+                    if (DEBUG_VOL) {
+                        Log.v(TAG, "readSettings: found stored index " + getValidIndex(index)
+                                 + " for group " + mAudioVolumeGroup.name() + ", device: " + name);
+                    }
+                    mIndexMap.put(device, getValidIndex(index));
+                }
+            }
+        }
+
+        private int getValidIndex(int index) {
+            if (index < mIndexMin) {
+                return mIndexMin;
+            } else if (mUseFixedVolume || index > mIndexMax) {
+                return mIndexMax;
+            }
+            return index;
+        }
+
+        public @NonNull String getSettingNameForDevice(int device) {
+            final String suffix = AudioSystem.getOutputDeviceName(device);
+            if (suffix.isEmpty()) {
+                return mAudioVolumeGroup.name();
+            }
+            return mAudioVolumeGroup.name() + "_" + AudioSystem.getOutputDeviceName(device);
+        }
+
+        private void dump(PrintWriter pw) {
+            pw.println("- VOLUME GROUP " + mAudioVolumeGroup.name() + ":");
+            pw.print("   Min: ");
+            pw.println(mIndexMin);
+            pw.print("   Max: ");
+            pw.println(mIndexMax);
+            pw.print("   Current: ");
+            for (int i = 0; i < mIndexMap.size(); i++) {
+                if (i > 0) {
+                    pw.print(", ");
+                }
+                final int device = mIndexMap.keyAt(i);
+                pw.print(Integer.toHexString(device));
+                final String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default"
+                        : AudioSystem.getOutputDeviceName(device);
+                if (!deviceName.isEmpty()) {
+                    pw.print(" (");
+                    pw.print(deviceName);
+                    pw.print(")");
+                }
+                pw.print(": ");
+                pw.print(mIndexMap.valueAt(i));
+            }
+            pw.println();
+            pw.print("   Devices: ");
+            int n = 0;
+            final int devices = getDeviceForVolume();
+            for (int device : AudioSystem.DEVICE_OUT_ALL_SET) {
+                if ((devices & device) == device) {
+                    if (n++ > 0) {
+                        pw.print(", ");
+                    }
+                    pw.print(AudioSystem.getOutputDeviceName(device));
+                }
+            }
+        }
+    }
+
+
+    // NOTE: Locking order for synchronized objects related to volume or ringer mode management:
+    //  1 mScoclient OR mSafeMediaVolumeState
+    //  2   mSetModeLock
+    //  3     mSettingsLock
+    //  4       VolumeStreamState.class
+    private class VolumeStreamState {
+        private final int mStreamType;
+        private int mIndexMin;
+        private int mIndexMax;
+
+        private boolean mIsMuted;
+        private String mVolumeIndexSettingName;
+        private int mObservedDevices;
+
+        private final SparseIntArray mIndexMap = new SparseIntArray(8) {
+            @Override
+            public void put(int key, int value) {
+                super.put(key, value);
+                record("put", key, value);
+            }
+            @Override
+            public void setValueAt(int index, int value) {
+                super.setValueAt(index, value);
+                record("setValueAt", keyAt(index), value);
+            }
+
+            // Record all changes in the VolumeStreamState
+            private void record(String event, int key, int value) {
+                final String device = key == AudioSystem.DEVICE_OUT_DEFAULT ? "default"
+                        : AudioSystem.getOutputDeviceName(key);
+                new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME + MediaMetrics.SEPARATOR
+                        + AudioSystem.streamToString(mStreamType)
+                        + "." + device)
+                        .set(MediaMetrics.Property.EVENT, event)
+                        .set(MediaMetrics.Property.INDEX, value)
+                        .set(MediaMetrics.Property.MIN_INDEX, mIndexMin)
+                        .set(MediaMetrics.Property.MAX_INDEX, mIndexMax)
+                        .record();
+            }
+        };
+        private final Intent mVolumeChanged;
+        private final Intent mStreamDevicesChanged;
+
+        private VolumeStreamState(String settingName, int streamType) {
+
+            mVolumeIndexSettingName = settingName;
+
+            mStreamType = streamType;
+            mIndexMin = MIN_STREAM_VOLUME[streamType] * 10;
+            mIndexMax = MAX_STREAM_VOLUME[streamType] * 10;
+            AudioSystem.initStreamVolume(streamType, mIndexMin / 10, mIndexMax / 10);
+
+            readSettings();
+            mVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
+            mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType);
+            mStreamDevicesChanged = new Intent(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
+            mStreamDevicesChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType);
+        }
+
+        public int observeDevicesForStream_syncVSS(boolean checkOthers) {
+            if (!mSystemServer.isPrivileged()) {
+                return AudioSystem.DEVICE_NONE;
+            }
+            final int devices = AudioSystem.getDevicesForStream(mStreamType);
+            if (devices == mObservedDevices) {
+                return devices;
+            }
+            final int prevDevices = mObservedDevices;
+            mObservedDevices = devices;
+            if (checkOthers) {
+                // one stream's devices have changed, check the others
+                observeDevicesForStreams(mStreamType);
+            }
+            // log base stream changes to the event log
+            if (mStreamVolumeAlias[mStreamType] == mStreamType) {
+                EventLogTags.writeStreamDevicesChanged(mStreamType, prevDevices, devices);
+            }
+            sendBroadcastToAll(mStreamDevicesChanged
+                    .putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_DEVICES, prevDevices)
+                    .putExtra(AudioManager.EXTRA_VOLUME_STREAM_DEVICES, devices));
+            return devices;
+        }
+
+        public @Nullable String getSettingNameForDevice(int device) {
+            if (!hasValidSettingsName()) {
+                return null;
+            }
+            final String suffix = AudioSystem.getOutputDeviceName(device);
+            if (suffix.isEmpty()) {
+                return mVolumeIndexSettingName;
+            }
+            return mVolumeIndexSettingName + "_" + suffix;
+        }
+
+        private boolean hasValidSettingsName() {
+            return (mVolumeIndexSettingName != null && !mVolumeIndexSettingName.isEmpty());
+        }
+
+        public void readSettings() {
+            synchronized (mSettingsLock) {
+                synchronized (VolumeStreamState.class) {
+                    // force maximum volume on all streams if fixed volume property is set
+                    if (mUseFixedVolume) {
+                        mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax);
+                        return;
+                    }
+                    // do not read system stream volume from settings: this stream is always aliased
+                    // to another stream type and its volume is never persisted. Values in settings can
+                    // only be stale values
+                    if ((mStreamType == AudioSystem.STREAM_SYSTEM) ||
+                            (mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED)) {
+                        int index = 10 * AudioSystem.DEFAULT_STREAM_VOLUME[mStreamType];
+                        if (mCameraSoundForced) {
+                            index = mIndexMax;
+                        }
+                        mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, index);
+                        return;
+                    }
+                }
+            }
+            synchronized (VolumeStreamState.class) {
+                for (int device : AudioSystem.DEVICE_OUT_ALL_SET) {
+
+                    // retrieve current volume for device
+                    // if no volume stored for current stream and device, use default volume if default
+                    // device, continue otherwise
+                    int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT) ?
+                            AudioSystem.DEFAULT_STREAM_VOLUME[mStreamType] : -1;
+                    int index;
+                    if (!hasValidSettingsName()) {
+                        index = defaultIndex;
+                    } else {
+                        String name = getSettingNameForDevice(device);
+                        index = Settings.System.getIntForUser(
+                                mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT);
+                    }
+                    if (index == -1) {
+                        continue;
+                    }
+
+                    mIndexMap.put(device, getValidIndex(10 * index));
+                }
+            }
+        }
+
+        private int getAbsoluteVolumeIndex(int index) {
+            /* Special handling for Bluetooth Absolute Volume scenario
+             * If we send full audio gain, some accessories are too loud even at its lowest
+             * volume. We are not able to enumerate all such accessories, so here is the
+             * workaround from phone side.
+             * Pre-scale volume at lowest volume steps 1 2 and 3.
+             * For volume step 0, set audio gain to 0 as some accessories won't mute on their end.
+             */
+            if (index == 0) {
+                // 0% for volume 0
+                index = 0;
+            } else if (index > 0 && index <= 3) {
+                // Pre-scale for volume steps 1 2 and 3
+                index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10;
+            } else {
+                // otherwise, full gain
+                index = (mIndexMax + 5) / 10;
+            }
+            return index;
+        }
+
+        private void setStreamVolumeIndex(int index, int device) {
+            // Only set audio policy BT SCO stream volume to 0 when the stream is actually muted.
+            // This allows RX path muting by the audio HAL only when explicitly muted but not when
+            // index is just set to 0 to repect BT requirements
+            if (mStreamType == AudioSystem.STREAM_BLUETOOTH_SCO && index == 0 && !mIsMuted) {
+                index = 1;
+            }
+            AudioSystem.setStreamVolumeIndexAS(mStreamType, index, device);
+        }
+
+        // must be called while synchronized VolumeStreamState.class
+        /*package*/ void applyDeviceVolume_syncVSS(int device, boolean isAvrcpAbsVolSupported) {
+            int index;
+            if (mIsMuted) {
+                index = 0;
+            } else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
+                    && isAvrcpAbsVolSupported) {
+                index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
+            } else if (isFullVolumeDevice(device)) {
+                index = (mIndexMax + 5)/10;
+            } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
+                index = (mIndexMax + 5)/10;
+            } else {
+                index = (getIndex(device) + 5)/10;
+            }
+            setStreamVolumeIndex(index, device);
+        }
+
+        public void applyAllVolumes() {
+            final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported();
+            synchronized (VolumeStreamState.class) {
+                // apply device specific volumes first
+                int index;
+                for (int i = 0; i < mIndexMap.size(); i++) {
+                    final int device = mIndexMap.keyAt(i);
+                    if (device != AudioSystem.DEVICE_OUT_DEFAULT) {
+                        if (mIsMuted) {
+                            index = 0;
+                        } else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
+                                && isAvrcpAbsVolSupported) {
+                            index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
+                        } else if (isFullVolumeDevice(device)) {
+                            index = (mIndexMax + 5)/10;
+                        } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
+                            index = (mIndexMax + 5)/10;
+                        } else {
+                            index = (mIndexMap.valueAt(i) + 5)/10;
+                        }
+                        setStreamVolumeIndex(index, device);
+                    }
+                }
+                // apply default volume last: by convention , default device volume will be used
+                // by audio policy manager if no explicit volume is present for a given device type
+                if (mIsMuted) {
+                    index = 0;
+                } else {
+                    index = (getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5)/10;
+                }
+                setStreamVolumeIndex(index, AudioSystem.DEVICE_OUT_DEFAULT);
+            }
+        }
+
+        public boolean adjustIndex(int deltaIndex, int device, String caller) {
+            return setIndex(getIndex(device) + deltaIndex, device, caller);
+        }
+
+        public boolean setIndex(int index, int device, String caller) {
+            boolean changed;
+            int oldIndex;
+            synchronized (mSettingsLock) {
+                synchronized (VolumeStreamState.class) {
+                    oldIndex = getIndex(device);
+                    index = getValidIndex(index);
+                    if ((mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) && mCameraSoundForced) {
+                        index = mIndexMax;
+                    }
+                    mIndexMap.put(device, index);
+
+                    changed = oldIndex != index;
+                    // Apply change to all streams using this one as alias if:
+                    // - the index actually changed OR
+                    // - there is no volume index stored for this device on alias stream.
+                    // If changing volume of current device, also change volume of current
+                    // device on aliased stream
+                    final boolean isCurrentDevice = (device == getDeviceForStream(mStreamType));
+                    final int numStreamTypes = AudioSystem.getNumStreamTypes();
+                    for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+                        final VolumeStreamState aliasStreamState = mStreamStates[streamType];
+                        if (streamType != mStreamType &&
+                                mStreamVolumeAlias[streamType] == mStreamType &&
+                                (changed || !aliasStreamState.hasIndexForDevice(device))) {
+                            final int scaledIndex = rescaleIndex(index, mStreamType, streamType);
+                            aliasStreamState.setIndex(scaledIndex, device, caller);
+                            if (isCurrentDevice) {
+                                aliasStreamState.setIndex(scaledIndex,
+                                        getDeviceForStream(streamType), caller);
+                            }
+                        }
+                    }
+                    // Mirror changes in SPEAKER ringtone volume on SCO when
+                    if (changed && mStreamType == AudioSystem.STREAM_RING
+                            && device == AudioSystem.DEVICE_OUT_SPEAKER) {
+                        for (int i = 0; i < mIndexMap.size(); i++) {
+                            int otherDevice = mIndexMap.keyAt(i);
+                            if (AudioSystem.DEVICE_OUT_ALL_SCO_SET.contains(otherDevice)) {
+                                mIndexMap.put(otherDevice, index);
+                            }
+                        }
+                    }
+                }
+            }
+            if (changed) {
+                oldIndex = (oldIndex + 5) / 10;
+                index = (index + 5) / 10;
+                // log base stream changes to the event log
+                if (mStreamVolumeAlias[mStreamType] == mStreamType) {
+                    if (caller == null) {
+                        Log.w(TAG, "No caller for volume_changed event", new Throwable());
+                    }
+                    EventLogTags.writeVolumeChanged(mStreamType, oldIndex, index, mIndexMax / 10,
+                            caller);
+                }
+                // fire changed intents for all streams
+                mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
+                mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
+                mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS,
+                        mStreamVolumeAlias[mStreamType]);
+                sendBroadcastToAll(mVolumeChanged);
+            }
+            return changed;
+        }
+
+        public int getIndex(int device) {
+            synchronized (VolumeStreamState.class) {
+                int index = mIndexMap.get(device, -1);
+                if (index == -1) {
+                    // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
+                    index = mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT);
+                }
+                return index;
+            }
+        }
+
+        public boolean hasIndexForDevice(int device) {
+            synchronized (VolumeStreamState.class) {
+                return (mIndexMap.get(device, -1) != -1);
+            }
+        }
+
+        public int getMaxIndex() {
+            return mIndexMax;
+        }
+
+        public int getMinIndex() {
+            return mIndexMin;
+        }
+
+        /**
+         * Copies all device/index pairs from the given VolumeStreamState after initializing
+         * them with the volume for DEVICE_OUT_DEFAULT. No-op if the source VolumeStreamState
+         * has the same stream type as this instance.
+         * @param srcStream
+         * @param caller
+         */
+        // must be sync'd on mSettingsLock before VolumeStreamState.class
+        @GuardedBy("VolumeStreamState.class")
+        public void setAllIndexes(VolumeStreamState srcStream, String caller) {
+            if (mStreamType == srcStream.mStreamType) {
+                return;
+            }
+            int srcStreamType = srcStream.getStreamType();
+            // apply default device volume from source stream to all devices first in case
+            // some devices are present in this stream state but not in source stream state
+            int index = srcStream.getIndex(AudioSystem.DEVICE_OUT_DEFAULT);
+            index = rescaleIndex(index, srcStreamType, mStreamType);
+            for (int i = 0; i < mIndexMap.size(); i++) {
+                mIndexMap.put(mIndexMap.keyAt(i), index);
+            }
+            // Now apply actual volume for devices in source stream state
+            SparseIntArray srcMap = srcStream.mIndexMap;
+            for (int i = 0; i < srcMap.size(); i++) {
+                int device = srcMap.keyAt(i);
+                index = srcMap.valueAt(i);
+                index = rescaleIndex(index, srcStreamType, mStreamType);
+
+                setIndex(index, device, caller);
+            }
+        }
+
+        // must be sync'd on mSettingsLock before VolumeStreamState.class
+        @GuardedBy("VolumeStreamState.class")
+        public void setAllIndexesToMax() {
+            for (int i = 0; i < mIndexMap.size(); i++) {
+                mIndexMap.put(mIndexMap.keyAt(i), mIndexMax);
+            }
+        }
+
+        /**
+         * Mute/unmute the stream
+         * @param state the new mute state
+         * @return true if the mute state was changed
+         */
+        public boolean mute(boolean state) {
+            boolean changed = false;
+            synchronized (VolumeStreamState.class) {
+                if (state != mIsMuted) {
+                    changed = true;
+                    mIsMuted = state;
+
+                    // Set the new mute volume. This propagates the values to
+                    // the audio system, otherwise the volume won't be changed
+                    // at the lower level.
+                    sendMsg(mAudioHandler,
+                            MSG_SET_ALL_VOLUMES,
+                            SENDMSG_QUEUE,
+                            0,
+                            0,
+                            this, 0);
+                }
+            }
+            if (changed) {
+                // Stream mute changed, fire the intent.
+                Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+                intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType);
+                intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, state);
+                sendBroadcastToAll(intent);
+            }
+            return changed;
+        }
+
+        public int getStreamType() {
+            return mStreamType;
+        }
+
+        public void checkFixedVolumeDevices() {
+            final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported();
+            synchronized (VolumeStreamState.class) {
+                // ignore settings for fixed volume devices: volume should always be at max or 0
+                if (mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_MUSIC) {
+                    for (int i = 0; i < mIndexMap.size(); i++) {
+                        int device = mIndexMap.keyAt(i);
+                        int index = mIndexMap.valueAt(i);
+                        if (isFullVolumeDevice(device)
+                                || (isFixedVolumeDevice(device) && index != 0)) {
+                            mIndexMap.put(device, mIndexMax);
+                        }
+                        applyDeviceVolume_syncVSS(device, isAvrcpAbsVolSupported);
+                    }
+                }
+            }
+        }
+
+        private int getValidIndex(int index) {
+            if (index < mIndexMin) {
+                return mIndexMin;
+            } else if (mUseFixedVolume || index > mIndexMax) {
+                return mIndexMax;
+            }
+
+            return index;
+        }
+
+        private void dump(PrintWriter pw) {
+            pw.print("   Muted: ");
+            pw.println(mIsMuted);
+            pw.print("   Min: ");
+            pw.println((mIndexMin + 5) / 10);
+            pw.print("   Max: ");
+            pw.println((mIndexMax + 5) / 10);
+            pw.print("   streamVolume:"); pw.println(getStreamVolume(mStreamType));
+            pw.print("   Current: ");
+            for (int i = 0; i < mIndexMap.size(); i++) {
+                if (i > 0) {
+                    pw.print(", ");
+                }
+                final int device = mIndexMap.keyAt(i);
+                pw.print(Integer.toHexString(device));
+                final String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default"
+                        : AudioSystem.getOutputDeviceName(device);
+                if (!deviceName.isEmpty()) {
+                    pw.print(" (");
+                    pw.print(deviceName);
+                    pw.print(")");
+                }
+                pw.print(": ");
+                final int index = (mIndexMap.valueAt(i) + 5) / 10;
+                pw.print(index);
+            }
+            pw.println();
+            pw.print("   Devices: ");
+            final int devices = getDevicesForStream(mStreamType);
+            int device, i = 0, n = 0;
+            // iterate all devices from 1 to DEVICE_OUT_DEFAULT exclusive
+            // (the default device is not returned by getDevicesForStream)
+            while ((device = 1 << i) != AudioSystem.DEVICE_OUT_DEFAULT) {
+                if ((devices & device) != 0) {
+                    if (n++ > 0) {
+                        pw.print(", ");
+                    }
+                    pw.print(AudioSystem.getOutputDeviceName(device));
+                }
+                i++;
+            }
+        }
+    }
+
+    /** Thread that handles native AudioSystem control. */
+    private class AudioSystemThread extends Thread {
+        AudioSystemThread() {
+            super("AudioService");
+        }
+
+        @Override
+        public void run() {
+            // Set this thread up so the handler will work on it
+            Looper.prepare();
+
+            synchronized(AudioService.this) {
+                mAudioHandler = new AudioHandler();
+
+                // Notify that the handler has been created
+                AudioService.this.notify();
+            }
+
+            // Listen for volume change requests that are set by VolumePanel
+            Looper.loop();
+        }
+    }
+
+    private static final class DeviceVolumeUpdate {
+        final int mStreamType;
+        final int mDevice;
+        final @NonNull String mCaller;
+        private static final int NO_NEW_INDEX = -2049;
+        private final int mVssVolIndex;
+
+        // Constructor with volume index, meant to cause this volume to be set and applied for the
+        // given stream type on the given device
+        DeviceVolumeUpdate(int streamType, int vssVolIndex, int device, @NonNull String caller) {
+            mStreamType = streamType;
+            mVssVolIndex = vssVolIndex;
+            mDevice = device;
+            mCaller = caller;
+        }
+
+        // Constructor with no volume index, meant to cause re-apply of volume for the given
+        // stream type on the given device
+        DeviceVolumeUpdate(int streamType, int device, @NonNull String caller) {
+            mStreamType = streamType;
+            mVssVolIndex = NO_NEW_INDEX;
+            mDevice = device;
+            mCaller = caller;
+        }
+
+        boolean hasVolumeIndex() {
+            return mVssVolIndex != NO_NEW_INDEX;
+        }
+
+        int getVolumeIndex() throws IllegalStateException {
+            Preconditions.checkState(mVssVolIndex != NO_NEW_INDEX);
+            return mVssVolIndex;
+        }
+    }
+
+    /** 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,
+                SENDMSG_QUEUE, 0 /*arg1*/, 0 /*arg2*/,
+                new DeviceVolumeUpdate(streamType, vssVolIndex, device, caller),
+                0 /*delay*/);
+    }
+
+    /*package*/ void postApplyVolumeOnDevice(int streamType, int device, @NonNull String caller) {
+        sendMsg(mAudioHandler,
+                MSG_SET_DEVICE_STREAM_VOLUME,
+                SENDMSG_QUEUE, 0 /*arg1*/, 0 /*arg2*/,
+                new DeviceVolumeUpdate(streamType, device, caller),
+                0 /*delay*/);
+    }
+
+    private void onSetVolumeIndexOnDevice(@NonNull DeviceVolumeUpdate update) {
+        final VolumeStreamState streamState = mStreamStates[update.mStreamType];
+        if (update.hasVolumeIndex()) {
+            final int index = update.getVolumeIndex();
+            streamState.setIndex(index, update.mDevice, update.mCaller);
+            sVolumeLogger.log(new AudioEventLogger.StringEvent(update.mCaller + " dev:0x"
+                    + Integer.toHexString(update.mDevice) + " volIdx:" + index));
+        } else {
+            sVolumeLogger.log(new AudioEventLogger.StringEvent(update.mCaller
+                    + " update vol on dev:0x" + Integer.toHexString(update.mDevice)));
+        }
+        setDeviceVolume(streamState, update.mDevice);
+    }
+
+    /*package*/ void setDeviceVolume(VolumeStreamState streamState, int device) {
+
+        final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported();
+
+        synchronized (VolumeStreamState.class) {
+            // Apply volume
+            streamState.applyDeviceVolume_syncVSS(device, isAvrcpAbsVolSupported);
+
+            // Apply change to all streams using this one as alias
+            int numStreamTypes = AudioSystem.getNumStreamTypes();
+            for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+                if (streamType != streamState.mStreamType &&
+                        mStreamVolumeAlias[streamType] == streamState.mStreamType) {
+                    // Make sure volume is also maxed out on A2DP device for aliased stream
+                    // that may have a different device selected
+                    int streamDevice = getDeviceForStream(streamType);
+                    if ((device != streamDevice) && isAvrcpAbsVolSupported
+                            && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)) {
+                        mStreamStates[streamType].applyDeviceVolume_syncVSS(device,
+                                isAvrcpAbsVolSupported);
+                    }
+                    mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice,
+                            isAvrcpAbsVolSupported);
+                }
+            }
+        }
+        // Post a persist volume msg
+        sendMsg(mAudioHandler,
+                MSG_PERSIST_VOLUME,
+                SENDMSG_QUEUE,
+                device,
+                0,
+                streamState,
+                PERSIST_DELAY);
+
+    }
+
+    /** Handles internal volume messages in separate volume thread. */
+    private class AudioHandler extends Handler {
+
+        private void setAllVolumes(VolumeStreamState streamState) {
+
+            // Apply volume
+            streamState.applyAllVolumes();
+
+            // Apply change to all streams using this one as alias
+            int numStreamTypes = AudioSystem.getNumStreamTypes();
+            for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+                if (streamType != streamState.mStreamType &&
+                        mStreamVolumeAlias[streamType] == streamState.mStreamType) {
+                    mStreamStates[streamType].applyAllVolumes();
+                }
+            }
+        }
+
+        private void persistVolume(VolumeStreamState streamState, int device) {
+            if (mUseFixedVolume) {
+                return;
+            }
+            if (mIsSingleVolume && (streamState.mStreamType != AudioSystem.STREAM_MUSIC)) {
+                return;
+            }
+            if (streamState.hasValidSettingsName()) {
+                System.putIntForUser(mContentResolver,
+                        streamState.getSettingNameForDevice(device),
+                        (streamState.getIndex(device) + 5)/ 10,
+                        UserHandle.USER_CURRENT);
+            }
+        }
+
+        private void persistRingerMode(int ringerMode) {
+            if (mUseFixedVolume) {
+                return;
+            }
+            Settings.Global.putInt(mContentResolver, Settings.Global.MODE_RINGER, ringerMode);
+        }
+
+        private void onPersistSafeVolumeState(int state) {
+            Settings.Global.putInt(mContentResolver,
+                    Settings.Global.AUDIO_SAFE_VOLUME_STATE,
+                    state);
+        }
+
+        private void onNotifyVolumeEvent(@NonNull IAudioPolicyCallback apc,
+                @AudioManager.VolumeAdjustment int direction) {
+            try {
+                apc.notifyVolumeAdjust(direction);
+            } catch(Exception e) {
+                // nothing we can do about this. Do not log error, too much potential for spam
+            }
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+
+                case MSG_SET_DEVICE_VOLUME:
+                    setDeviceVolume((VolumeStreamState) msg.obj, msg.arg1);
+                    break;
+
+                case MSG_SET_ALL_VOLUMES:
+                    setAllVolumes((VolumeStreamState) msg.obj);
+                    break;
+
+                case MSG_PERSIST_VOLUME:
+                    persistVolume((VolumeStreamState) msg.obj, msg.arg1);
+                    break;
+
+                case MSG_PERSIST_VOLUME_GROUP:
+                    final VolumeGroupState vgs = (VolumeGroupState) msg.obj;
+                    vgs.persistVolumeGroup(msg.arg1);
+                    break;
+
+                case MSG_PERSIST_RINGER_MODE:
+                    // note that the value persisted is the current ringer mode, not the
+                    // value of ringer mode as of the time the request was made to persist
+                    persistRingerMode(getRingerModeInternal());
+                    break;
+
+                case MSG_AUDIO_SERVER_DIED:
+                    onAudioServerDied();
+                    break;
+
+                case MSG_DISPATCH_AUDIO_SERVER_STATE:
+                    onDispatchAudioServerStateChange(msg.arg1 == 1);
+                    break;
+
+                case MSG_UNLOAD_SOUND_EFFECTS:
+                    mSfxHelper.unloadSoundEffects();
+                    break;
+
+                case MSG_LOAD_SOUND_EFFECTS:
+                {
+                    LoadSoundEffectReply reply = (LoadSoundEffectReply) msg.obj;
+                    if (mSystemReady) {
+                        mSfxHelper.loadSoundEffects(reply);
+                    } else {
+                        Log.w(TAG, "[schedule]loadSoundEffects() called before boot complete");
+                        if (reply != null) {
+                            reply.run(false);
+                        }
+                    }
+                }
+                    break;
+
+                case MSG_PLAY_SOUND_EFFECT:
+                    mSfxHelper.playSoundEffect(msg.arg1, msg.arg2);
+                    break;
+
+                case MSG_SET_FORCE_USE:
+                {
+                    final String eventSource = (String) msg.obj;
+                    final int useCase = msg.arg1;
+                    final int config = msg.arg2;
+                    if (useCase == AudioSystem.FOR_MEDIA) {
+                        Log.wtf(TAG, "Invalid force use FOR_MEDIA in AudioService from "
+                                + eventSource);
+                        break;
+                    }
+                    new MediaMetrics.Item(MediaMetrics.Name.AUDIO_FORCE_USE
+                            + MediaMetrics.SEPARATOR + AudioSystem.forceUseUsageToString(useCase))
+                            .set(MediaMetrics.Property.EVENT, "setForceUse")
+                            .set(MediaMetrics.Property.FORCE_USE_DUE_TO, eventSource)
+                            .set(MediaMetrics.Property.FORCE_USE_MODE,
+                                    AudioSystem.forceUseConfigToString(config))
+                            .record();
+                    sForceUseLogger.log(
+                            new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource));
+                    AudioSystem.setForceUse(useCase, config);
+                }
+                    break;
+
+                case MSG_DISABLE_AUDIO_FOR_UID:
+                    mPlaybackMonitor.disableAudioForUid( msg.arg1 == 1 /* disable */,
+                            msg.arg2 /* uid */);
+                    mAudioEventWakeLock.release();
+                    break;
+
+                case MSG_CHECK_MUSIC_ACTIVE:
+                    onCheckMusicActive((String) msg.obj);
+                    break;
+
+                case MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED:
+                case MSG_CONFIGURE_SAFE_MEDIA_VOLUME:
+                    onConfigureSafeVolume((msg.what == MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED),
+                            (String) msg.obj);
+                    break;
+                case MSG_PERSIST_SAFE_VOLUME_STATE:
+                    onPersistSafeVolumeState(msg.arg1);
+                    break;
+
+                case MSG_SYSTEM_READY:
+                    onSystemReady();
+                    break;
+
+                case MSG_INDICATE_SYSTEM_READY:
+                    onIndicateSystemReady();
+                    break;
+
+                case MSG_ACCESSORY_PLUG_MEDIA_UNMUTE:
+                    onAccessoryPlugMediaUnmute(msg.arg1);
+                    break;
+
+                case MSG_PERSIST_MUSIC_ACTIVE_MS:
+                    final int musicActiveMs = msg.arg1;
+                    Settings.Secure.putIntForUser(mContentResolver,
+                            Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, musicActiveMs,
+                            UserHandle.USER_CURRENT);
+                    break;
+
+                case MSG_UNMUTE_STREAM:
+                    onUnmuteStream(msg.arg1, msg.arg2);
+                    break;
+
+                case MSG_DYN_POLICY_MIX_STATE_UPDATE:
+                    onDynPolicyMixStateUpdate((String) msg.obj, msg.arg1);
+                    break;
+
+                case MSG_NOTIFY_VOL_EVENT:
+                    onNotifyVolumeEvent((IAudioPolicyCallback) msg.obj, msg.arg1);
+                    break;
+
+                case MSG_ENABLE_SURROUND_FORMATS:
+                    onEnableSurroundFormats((ArrayList<Integer>) msg.obj);
+                    break;
+
+                case MSG_UPDATE_RINGER_MODE:
+                    onUpdateRingerModeServiceInt();
+                    break;
+
+                case MSG_SET_DEVICE_STREAM_VOLUME:
+                    onSetVolumeIndexOnDevice((DeviceVolumeUpdate) msg.obj);
+                    break;
+
+                case MSG_OBSERVE_DEVICES_FOR_ALL_STREAMS:
+                    onObserveDevicesForAllStreams();
+                    break;
+
+                case MSG_HDMI_VOLUME_CHECK:
+                    onCheckVolumeCecOnHdmiConnection(msg.arg1, (String) msg.obj);
+                    break;
+
+                case MSG_PLAYBACK_CONFIG_CHANGE:
+                    onPlaybackConfigChange((List<AudioPlaybackConfiguration>) msg.obj);
+                    break;
+
+                case MSG_BROADCAST_MICROPHONE_MUTE:
+                    mSystemServer.sendMicrophoneMuteChangedIntent();
+                    break;
+            }
+        }
+    }
+
+    private class SettingsObserver extends ContentObserver {
+
+        SettingsObserver() {
+            super(new Handler());
+            mContentResolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.ZEN_MODE), false, this);
+            mContentResolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.ZEN_MODE_CONFIG_ETAG), false, this);
+            mContentResolver.registerContentObserver(Settings.System.getUriFor(
+                Settings.System.MODE_RINGER_STREAMS_AFFECTED), false, this);
+            mContentResolver.registerContentObserver(Settings.Global.getUriFor(
+                Settings.Global.DOCK_AUDIO_MEDIA_ENABLED), false, this);
+            mContentResolver.registerContentObserver(Settings.System.getUriFor(
+                    Settings.System.MASTER_MONO), false, this);
+            mContentResolver.registerContentObserver(Settings.System.getUriFor(
+                    Settings.System.MASTER_BALANCE), false, this);
+
+            mEncodedSurroundMode = Settings.Global.getInt(
+                    mContentResolver, Settings.Global.ENCODED_SURROUND_OUTPUT,
+                    Settings.Global.ENCODED_SURROUND_OUTPUT_AUTO);
+            mContentResolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.ENCODED_SURROUND_OUTPUT), false, this);
+
+            mEnabledSurroundFormats = Settings.Global.getString(
+                    mContentResolver, Settings.Global.ENCODED_SURROUND_OUTPUT_ENABLED_FORMATS);
+            mContentResolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.ENCODED_SURROUND_OUTPUT_ENABLED_FORMATS), false, this);
+
+            mContentResolver.registerContentObserver(Settings.Secure.getUriFor(
+                    Settings.Secure.VOICE_INTERACTION_SERVICE), false, this);
+            mContentResolver.registerContentObserver(Settings.Secure.getUriFor(
+                    Settings.Secure.DEFAULT_INPUT_METHOD), false, this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            super.onChange(selfChange);
+            // FIXME This synchronized is not necessary if mSettingsLock only protects mRingerMode.
+            //       However there appear to be some missing locks around mRingerAndZenModeMutedStreams
+            //       and mRingerModeAffectedStreams, so will leave this synchronized for now.
+            //       mRingerAndZenModeMutedStreams and mMuteAffectedStreams are safe (only accessed once).
+            synchronized (mSettingsLock) {
+                if (updateRingerAndZenModeAffectedStreams()) {
+                    /*
+                     * Ensure all stream types that should be affected by ringer mode
+                     * are in the proper state.
+                     */
+                    setRingerModeInt(getRingerModeInternal(), false);
+                }
+                readDockAudioSettings(mContentResolver);
+                updateMasterMono(mContentResolver);
+                updateMasterBalance(mContentResolver);
+                updateEncodedSurroundOutput();
+                sendEnabledSurroundFormats(mContentResolver, mSurroundModeChanged);
+                updateAssistantUId(false);
+                updateCurrentImeUid(false);
+            }
+        }
+
+        private void updateEncodedSurroundOutput() {
+            int newSurroundMode = Settings.Global.getInt(
+                mContentResolver, Settings.Global.ENCODED_SURROUND_OUTPUT,
+                Settings.Global.ENCODED_SURROUND_OUTPUT_AUTO);
+            // Did it change?
+            if (mEncodedSurroundMode != newSurroundMode) {
+                // Send to AudioPolicyManager
+                sendEncodedSurroundMode(newSurroundMode, "SettingsObserver");
+                mDeviceBroker.toggleHdmiIfConnected_Async();
+                mEncodedSurroundMode = newSurroundMode;
+                mSurroundModeChanged = true;
+            } else {
+                mSurroundModeChanged = false;
+            }
+        }
+    }
+
+    public void avrcpSupportsAbsoluteVolume(String address, boolean support) {
+        // address is not used for now, but may be used when multiple a2dp devices are supported
+        sVolumeLogger.log(new AudioEventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
+                + address + " support=" + support));
+        mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support);
+        sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE,
+                    AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0,
+                    mStreamStates[AudioSystem.STREAM_MUSIC], 0);
+    }
+
+    /**
+     * @return true if there is currently a registered dynamic mixing policy that affects media
+     * and is not a render + loopback policy
+     */
+    // 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,
+                        AudioMix.ROUTE_FLAG_LOOP_BACK_RENDER)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /*package*/ void checkMusicActive(int deviceType, String caller) {
+        if (mSafeMediaVolumeDevices.contains(deviceType)) {
+            sendMsg(mAudioHandler,
+                    MSG_CHECK_MUSIC_ACTIVE,
+                    SENDMSG_REPLACE,
+                    0,
+                    0,
+                    caller,
+                    MUSIC_ACTIVE_POLL_PERIOD_MS);
+        }
+    }
+
+    /**
+     * Receiver for misc intent broadcasts the Phone app cares about.
+     */
+    private class AudioServiceBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            int outDevice;
+            int inDevice;
+            int state;
+
+            if (action.equals(Intent.ACTION_DOCK_EVENT)) {
+                int dockState = intent.getIntExtra(Intent.EXTRA_DOCK_STATE,
+                        Intent.EXTRA_DOCK_STATE_UNDOCKED);
+                int config;
+                switch (dockState) {
+                    case Intent.EXTRA_DOCK_STATE_DESK:
+                        config = AudioSystem.FORCE_BT_DESK_DOCK;
+                        break;
+                    case Intent.EXTRA_DOCK_STATE_CAR:
+                        config = AudioSystem.FORCE_BT_CAR_DOCK;
+                        break;
+                    case Intent.EXTRA_DOCK_STATE_LE_DESK:
+                        config = AudioSystem.FORCE_ANALOG_DOCK;
+                        break;
+                    case Intent.EXTRA_DOCK_STATE_HE_DESK:
+                        config = AudioSystem.FORCE_DIGITAL_DOCK;
+                        break;
+                    case Intent.EXTRA_DOCK_STATE_UNDOCKED:
+                    default:
+                        config = AudioSystem.FORCE_NONE;
+                }
+                // Low end docks have a menu to enable or disable audio
+                // (see mDockAudioMediaEnabled)
+                if (!((dockState == Intent.EXTRA_DOCK_STATE_LE_DESK)
+                        || ((dockState == Intent.EXTRA_DOCK_STATE_UNDOCKED)
+                                && (mDockState == Intent.EXTRA_DOCK_STATE_LE_DESK)))) {
+                    mDeviceBroker.setForceUse_Async(AudioSystem.FOR_DOCK, config,
+                            "ACTION_DOCK_EVENT intent");
+                }
+                mDockState = dockState;
+            } else if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)
+                    || action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+                mDeviceBroker.receiveBtEvent(intent);
+            } else if (action.equals(Intent.ACTION_SCREEN_ON)) {
+                if (mMonitorRotation) {
+                    RotationHelper.enable();
+                }
+                AudioSystem.setParameters("screen_state=on");
+            } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
+                if (mMonitorRotation) {
+                    //reduce wakeups (save current) by only listening when display is on
+                    RotationHelper.disable();
+                }
+                AudioSystem.setParameters("screen_state=off");
+            } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+                handleConfigurationChanged(context);
+            } else if (action.equals(Intent.ACTION_USER_SWITCHED)) {
+                if (mUserSwitchedReceived) {
+                    // attempt to stop music playback for background user except on first user
+                    // switch (i.e. first boot)
+                    mDeviceBroker.postBroadcastBecomingNoisy();
+                }
+                mUserSwitchedReceived = true;
+                // the current audio focus owner is no longer valid
+                mMediaFocusControl.discardAudioFocusOwner();
+
+                // load volume settings for new user
+                readAudioSettings(true /*userSwitch*/);
+                // preserve STREAM_MUSIC volume from one user to the next.
+                sendMsg(mAudioHandler,
+                        MSG_SET_ALL_VOLUMES,
+                        SENDMSG_QUEUE,
+                        0,
+                        0,
+                        mStreamStates[AudioSystem.STREAM_MUSIC], 0);
+            } else if (action.equals(Intent.ACTION_USER_BACKGROUND)) {
+                // Disable audio recording for the background user/profile
+                int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                if (userId >= 0) {
+                    // TODO Kill recording streams instead of killing processes holding permission
+                    UserInfo userInfo = UserManagerService.getInstance().getUserInfo(userId);
+                    killBackgroundUserProcessesWithRecordAudioPermission(userInfo);
+                }
+                UserManagerService.getInstance().setUserRestriction(
+                        UserManager.DISALLOW_RECORD_AUDIO, true, userId);
+            } else if (action.equals(Intent.ACTION_USER_FOREGROUND)) {
+                // Enable audio recording for foreground user/profile
+                int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                UserManagerService.getInstance().setUserRestriction(
+                        UserManager.DISALLOW_RECORD_AUDIO, false, userId);
+            } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+                state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
+                if (state == BluetoothAdapter.STATE_OFF ||
+                        state == BluetoothAdapter.STATE_TURNING_OFF) {
+                    mDeviceBroker.disconnectAllBluetoothProfiles();
+                }
+            } else if (action.equals(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) ||
+                    action.equals(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)) {
+                handleAudioEffectBroadcast(context, intent);
+            } else if (action.equals(Intent.ACTION_PACKAGES_SUSPENDED)) {
+                final int[] suspendedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST);
+                final String[] suspendedPackages =
+                        intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+                if (suspendedPackages == null || suspendedUids == null
+                        || suspendedPackages.length != suspendedUids.length) {
+                    return;
+                }
+                for (int i = 0; i < suspendedUids.length; i++) {
+                    if (!TextUtils.isEmpty(suspendedPackages[i])) {
+                        mMediaFocusControl.noFocusForSuspendedApp(
+                                suspendedPackages[i], suspendedUids[i]);
+                    }
+                }
+            }
+        }
+    } // end class AudioServiceBroadcastReceiver
+
+    private class AudioServiceUserRestrictionsListener implements UserRestrictionsListener {
+
+        @Override
+        public void onUserRestrictionsChanged(int userId, Bundle newRestrictions,
+                Bundle prevRestrictions) {
+            // Update mic mute state.
+            {
+                final boolean wasRestricted =
+                        prevRestrictions.getBoolean(UserManager.DISALLOW_UNMUTE_MICROPHONE);
+                final boolean isRestricted =
+                        newRestrictions.getBoolean(UserManager.DISALLOW_UNMUTE_MICROPHONE);
+                if (wasRestricted != isRestricted) {
+                    mMicMuteFromRestrictions = isRestricted;
+                    setMicrophoneMuteNoCallerCheck(userId);
+                }
+            }
+
+            // Update speaker mute state.
+            {
+                final boolean wasRestricted =
+                        prevRestrictions.getBoolean(UserManager.DISALLOW_ADJUST_VOLUME)
+                                || prevRestrictions.getBoolean(UserManager.DISALLOW_UNMUTE_DEVICE);
+                final boolean isRestricted =
+                        newRestrictions.getBoolean(UserManager.DISALLOW_ADJUST_VOLUME)
+                                || newRestrictions.getBoolean(UserManager.DISALLOW_UNMUTE_DEVICE);
+                if (wasRestricted != isRestricted) {
+                    setMasterMuteInternalNoCallerCheck(isRestricted, /* flags =*/ 0, userId);
+                }
+            }
+        }
+    } // end class AudioServiceUserRestrictionsListener
+
+    private void handleAudioEffectBroadcast(Context context, Intent intent) {
+        String target = intent.getPackage();
+        if (target != null) {
+            Log.w(TAG, "effect broadcast already targeted to " + target);
+            return;
+        }
+        intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
+        // TODO this should target a user-selected panel
+        List<ResolveInfo> ril = context.getPackageManager().queryBroadcastReceivers(
+                intent, 0 /* flags */);
+        if (ril != null && ril.size() != 0) {
+            ResolveInfo ri = ril.get(0);
+            if (ri != null && ri.activityInfo != null && ri.activityInfo.packageName != null) {
+                intent.setPackage(ri.activityInfo.packageName);
+                context.sendBroadcastAsUser(intent, UserHandle.ALL);
+                return;
+            }
+        }
+        Log.w(TAG, "couldn't find receiver package for effect intent");
+    }
+
+    private void killBackgroundUserProcessesWithRecordAudioPermission(UserInfo oldUser) {
+        PackageManager pm = mContext.getPackageManager();
+        // Find the home activity of the user. It should not be killed to avoid expensive restart,
+        // when the user switches back. For managed profiles, we should kill all recording apps
+        ComponentName homeActivityName = null;
+        if (!oldUser.isManagedProfile()) {
+            homeActivityName = LocalServices.getService(
+                    ActivityTaskManagerInternal.class).getHomeActivityForUser(oldUser.id);
+        }
+        final String[] permissions = { Manifest.permission.RECORD_AUDIO };
+        List<PackageInfo> packages;
+        try {
+            packages = AppGlobals.getPackageManager()
+                    .getPackagesHoldingPermissions(permissions, 0, oldUser.id).getList();
+        } catch (RemoteException e) {
+            throw new AndroidRuntimeException(e);
+        }
+        for (int j = packages.size() - 1; j >= 0; j--) {
+            PackageInfo pkg = packages.get(j);
+            // Skip system processes
+            if (UserHandle.getAppId(pkg.applicationInfo.uid) < FIRST_APPLICATION_UID) {
+                continue;
+            }
+            // Skip packages that have permission to interact across users
+            if (pm.checkPermission(Manifest.permission.INTERACT_ACROSS_USERS, pkg.packageName)
+                    == PackageManager.PERMISSION_GRANTED) {
+                continue;
+            }
+            if (homeActivityName != null
+                    && pkg.packageName.equals(homeActivityName.getPackageName())
+                    && pkg.applicationInfo.isSystemApp()) {
+                continue;
+            }
+            try {
+                final int uid = pkg.applicationInfo.uid;
+                ActivityManager.getService().killUid(UserHandle.getAppId(uid),
+                        UserHandle.getUserId(uid),
+                        "killBackgroundUserProcessesWithAudioRecordPermission");
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling killUid", e);
+            }
+        }
+    }
+
+
+    //==========================================================================================
+    // Audio Focus
+    //==========================================================================================
+    /**
+     * Returns whether a focus request is eligible to force ducking.
+     * Will return true if:
+     * - the AudioAttributes have a usage of USAGE_ASSISTANCE_ACCESSIBILITY,
+     * - the focus request is AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+     * - the associated Bundle has KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING set to true,
+     * - the uid of the requester is a known accessibility service or root.
+     * @param aa AudioAttributes of the focus request
+     * @param uid uid of the focus requester
+     * @return true if ducking is to be forced
+     */
+    private boolean forceFocusDuckingForAccessibility(@Nullable AudioAttributes aa,
+            int request, int uid) {
+        if (aa == null || aa.getUsage() != AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
+                || request != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) {
+            return false;
+        }
+        final Bundle extraInfo = aa.getBundle();
+        if (extraInfo == null ||
+                !extraInfo.getBoolean(AudioFocusRequest.KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING)) {
+            return false;
+        }
+        if (uid == 0) {
+            return true;
+        }
+        synchronized (mAccessibilityServiceUidsLock) {
+            if (mAccessibilityServiceUids != null) {
+                int callingUid = Binder.getCallingUid();
+                for (int i = 0; i < mAccessibilityServiceUids.length; i++) {
+                    if (mAccessibilityServiceUids[i] == callingUid) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isSupportedSystemUsage(@AudioAttributes.AttributeUsage int usage) {
+        synchronized (mSupportedSystemUsagesLock) {
+            for (int i = 0; i < mSupportedSystemUsages.length; i++) {
+                if (mSupportedSystemUsages[i] == usage) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    private void validateAudioAttributesUsage(@NonNull AudioAttributes audioAttributes) {
+        @AudioAttributes.AttributeUsage int usage = audioAttributes.getSystemUsage();
+        if (AudioAttributes.isSystemUsage(usage)) {
+            if (callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)) {
+                if (!isSupportedSystemUsage(usage)) {
+                    throw new IllegalArgumentException(
+                            "Unsupported usage " + AudioAttributes.usageToString(usage));
+                }
+            } else {
+                throw new SecurityException("Missing MODIFY_AUDIO_ROUTING permission");
+            }
+        }
+    }
+
+    private boolean isValidAudioAttributesUsage(@NonNull AudioAttributes audioAttributes) {
+        @AudioAttributes.AttributeUsage int usage = audioAttributes.getSystemUsage();
+        if (AudioAttributes.isSystemUsage(usage)) {
+            return callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
+                    && isSupportedSystemUsage(usage);
+        }
+        return true;
+    }
+
+    public int requestAudioFocus(AudioAttributes aa, int durationHint, IBinder cb,
+            IAudioFocusDispatcher fd, String clientId, String callingPackageName, int flags,
+            IAudioPolicyCallback pcb, int sdk) {
+        final int uid = Binder.getCallingUid();
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "focus")
+                .setUid(uid)
+                //.putInt("durationHint", durationHint)
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "requestAudioFocus")
+                .set(MediaMetrics.Property.FLAGS, flags);
+
+        // permission checks
+        if (aa != null && !isValidAudioAttributesUsage(aa)) {
+            final String reason = "Request using unsupported usage";
+            Log.w(TAG, reason);
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                    .record();
+            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        }
+        if ((flags & AudioManager.AUDIOFOCUS_FLAG_LOCK) == AudioManager.AUDIOFOCUS_FLAG_LOCK) {
+            if (AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId)) {
+                if (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission(
+                            android.Manifest.permission.MODIFY_PHONE_STATE)) {
+                    final String reason = "Invalid permission to (un)lock audio focus";
+                    Log.e(TAG, reason, new Exception());
+                    mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                            .record();
+                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+                }
+            } else {
+                // only a registered audio policy can be used to lock focus
+                synchronized (mAudioPolicies) {
+                    if (!mAudioPolicies.containsKey(pcb.asBinder())) {
+                        final String reason =
+                                "Invalid unregistered AudioPolicy to (un)lock audio focus";
+                        Log.e(TAG, reason);
+                        mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                                .record();
+                        return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+                    }
+                }
+            }
+        }
+
+        if (callingPackageName == null || clientId == null || aa == null) {
+            final String reason = "Invalid null parameter to request audio focus";
+            Log.e(TAG, reason);
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, reason)
+                    .record();
+            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        }
+        mmi.record();
+        return mMediaFocusControl.requestAudioFocus(aa, durationHint, cb, fd,
+                clientId, callingPackageName, flags, sdk,
+                forceFocusDuckingForAccessibility(aa, durationHint, uid));
+    }
+
+    public int abandonAudioFocus(IAudioFocusDispatcher fd, String clientId, AudioAttributes aa,
+            String callingPackageName) {
+        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "focus")
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "abandonAudioFocus");
+
+        if (aa != null && !isValidAudioAttributesUsage(aa)) {
+            Log.w(TAG, "Request using unsupported usage.");
+            mmi.set(MediaMetrics.Property.EARLY_RETURN, "unsupported usage").record();
+
+            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        }
+        mmi.record();
+        return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName);
+    }
+
+    public void unregisterAudioFocusClient(String clientId) {
+        new MediaMetrics.Item(mMetricsId + "focus")
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "unregisterAudioFocusClient")
+                .record();
+        mMediaFocusControl.unregisterAudioFocusClient(clientId);
+    }
+
+    public int getCurrentAudioFocus() {
+        return mMediaFocusControl.getCurrentAudioFocus();
+    }
+
+    public int getFocusRampTimeMs(int focusGain, AudioAttributes attr) {
+        return mMediaFocusControl.getFocusRampTimeMs(focusGain, attr);
+    }
+
+    /** only public for mocking/spying, do not call outside of AudioService */
+    @VisibleForTesting
+    public boolean hasAudioFocusUsers() {
+        return mMediaFocusControl.hasAudioFocusUsers();
+    }
+
+    //==========================================================================================
+    private boolean readCameraSoundForced() {
+        return SystemProperties.getBoolean("audio.camerasound.force", false) ||
+                mContext.getResources().getBoolean(
+                        com.android.internal.R.bool.config_camera_sound_forced);
+    }
+
+    //==========================================================================================
+    // Device orientation
+    //==========================================================================================
+    /**
+     * Handles device configuration changes that may map to a change in rotation.
+     * Monitoring rotation is optional, and is defined by the definition and value
+     * of the "ro.audio.monitorRotation" system property.
+     */
+    private void handleConfigurationChanged(Context context) {
+        try {
+            // reading new configuration "safely" (i.e. under try catch) in case anything
+            // goes wrong.
+            Configuration config = context.getResources().getConfiguration();
+            sendMsg(mAudioHandler,
+                    MSG_CONFIGURE_SAFE_MEDIA_VOLUME,
+                    SENDMSG_REPLACE,
+                    0,
+                    0,
+                    TAG,
+                    0);
+
+            boolean cameraSoundForced = readCameraSoundForced();
+            synchronized (mSettingsLock) {
+                final boolean cameraSoundForcedChanged = (cameraSoundForced != mCameraSoundForced);
+                mCameraSoundForced = cameraSoundForced;
+                if (cameraSoundForcedChanged) {
+                    if (!mIsSingleVolume) {
+                        synchronized (VolumeStreamState.class) {
+                            VolumeStreamState s = mStreamStates[AudioSystem.STREAM_SYSTEM_ENFORCED];
+                            if (cameraSoundForced) {
+                                s.setAllIndexesToMax();
+                                mRingerModeAffectedStreams &=
+                                        ~(1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
+                            } else {
+                                s.setAllIndexes(mStreamStates[AudioSystem.STREAM_SYSTEM], TAG);
+                                mRingerModeAffectedStreams |=
+                                        (1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
+                            }
+                        }
+                        // take new state into account for streams muted by ringer mode
+                        setRingerModeInt(getRingerModeInternal(), false);
+                    }
+                    mDeviceBroker.setForceUse_Async(AudioSystem.FOR_SYSTEM,
+                            cameraSoundForced ?
+                                    AudioSystem.FORCE_SYSTEM_ENFORCED : AudioSystem.FORCE_NONE,
+                            "handleConfigurationChanged");
+                    sendMsg(mAudioHandler,
+                            MSG_SET_ALL_VOLUMES,
+                            SENDMSG_QUEUE,
+                            0,
+                            0,
+                            mStreamStates[AudioSystem.STREAM_SYSTEM_ENFORCED], 0);
+
+                }
+            }
+            mVolumeController.setLayoutDirection(config.getLayoutDirection());
+        } catch (Exception e) {
+            Log.e(TAG, "Error handling configuration change: ", e);
+        }
+    }
+
+    @Override
+    public void setRingtonePlayer(IRingtonePlayer player) {
+        mContext.enforceCallingOrSelfPermission(REMOTE_AUDIO_PLAYBACK, null);
+        mRingtonePlayer = player;
+    }
+
+    @Override
+    public IRingtonePlayer getRingtonePlayer() {
+        return mRingtonePlayer;
+    }
+
+    @Override
+    public AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
+        return mDeviceBroker.startWatchingRoutes(observer);
+    }
+
+
+    //==========================================================================================
+    // Safe media volume management.
+    // MUSIC stream volume level is limited when headphones are connected according to safety
+    // regulation. When the user attempts to raise the volume above the limit, a warning is
+    // displayed and the user has to acknowlegde before the volume is actually changed.
+    // The volume index corresponding to the limit is stored in config_safe_media_volume_index
+    // property. Platforms with a different limit must set this property accordingly in their
+    // overlay.
+    //==========================================================================================
+
+    // mSafeMediaVolumeState indicates whether the media volume is limited over headphones.
+    // It is SAFE_MEDIA_VOLUME_NOT_CONFIGURED at boot time until a network service is connected
+    // or the configure time is elapsed. It is then set to SAFE_MEDIA_VOLUME_ACTIVE or
+    // SAFE_MEDIA_VOLUME_DISABLED according to country option. If not SAFE_MEDIA_VOLUME_DISABLED, it
+    // can be set to SAFE_MEDIA_VOLUME_INACTIVE by calling AudioService.disableSafeMediaVolume()
+    // (when user opts out).
+    private static final int SAFE_MEDIA_VOLUME_NOT_CONFIGURED = 0;
+    private static final int SAFE_MEDIA_VOLUME_DISABLED = 1;
+    private static final int SAFE_MEDIA_VOLUME_INACTIVE = 2;  // confirmed
+    private static final int SAFE_MEDIA_VOLUME_ACTIVE = 3;  // unconfirmed
+    private int mSafeMediaVolumeState;
+    private final Object mSafeMediaVolumeStateLock = new Object();
+
+    private int mMcc = 0;
+    // mSafeMediaVolumeIndex is the cached value of config_safe_media_volume_index property
+    private int mSafeMediaVolumeIndex;
+    // mSafeUsbMediaVolumeDbfs is the cached value of the config_safe_media_volume_usb_mB
+    // property, divided by 100.0.
+    private float mSafeUsbMediaVolumeDbfs;
+    // mSafeUsbMediaVolumeIndex is used for USB Headsets and is the music volume UI index
+    // corresponding to a gain of mSafeUsbMediaVolumeDbfs (defaulting to -37dB) in audio
+    // flinger mixer.
+    // We remove -22 dBs from the theoretical -15dB to account for the EQ + bass boost
+    // amplification when both effects are on with all band gains at maximum.
+    // This level corresponds to a loudness of 85 dB SPL for the warning to be displayed when
+    // the headset is compliant to EN 60950 with a max loudness of 100dB SPL.
+    private int mSafeUsbMediaVolumeIndex;
+    // mSafeMediaVolumeDevices lists the devices for which safe media volume is enforced,
+    /*package*/ final Set<Integer> mSafeMediaVolumeDevices = new HashSet<>(
+            Arrays.asList(AudioSystem.DEVICE_OUT_WIRED_HEADSET,
+                    AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, AudioSystem.DEVICE_OUT_USB_HEADSET));
+    // mMusicActiveMs is the cumulative time of music activity since safe volume was disabled.
+    // When this time reaches UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX, the safe media volume is re-enabled
+    // automatically. mMusicActiveMs is rounded to a multiple of MUSIC_ACTIVE_POLL_PERIOD_MS.
+    private int mMusicActiveMs;
+    private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
+    private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000;  // 1 minute polling interval
+    private static final int SAFE_VOLUME_CONFIGURE_TIMEOUT_MS = 30000;  // 30s after boot completed
+
+    private int safeMediaVolumeIndex(int device) {
+        if (!mSafeMediaVolumeDevices.contains(device)) {
+            return MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
+        }
+        if (device == AudioSystem.DEVICE_OUT_USB_HEADSET) {
+            return mSafeUsbMediaVolumeIndex;
+        } else {
+            return mSafeMediaVolumeIndex;
+        }
+    }
+
+    private void setSafeMediaVolumeEnabled(boolean on, String caller) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            if ((mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_NOT_CONFIGURED) &&
+                    (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_DISABLED)) {
+                if (on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE)) {
+                    mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
+                    enforceSafeMediaVolume(caller);
+                } else if (!on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)) {
+                    mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
+                    mMusicActiveMs = 1;  // nonzero = confirmed
+                    saveMusicActiveMs();
+                    sendMsg(mAudioHandler,
+                            MSG_CHECK_MUSIC_ACTIVE,
+                            SENDMSG_REPLACE,
+                            0,
+                            0,
+                            caller,
+                            MUSIC_ACTIVE_POLL_PERIOD_MS);
+                }
+            }
+        }
+    }
+
+    private void enforceSafeMediaVolume(String caller) {
+        VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
+        Set<Integer> devices = mSafeMediaVolumeDevices;
+
+        for (int device : devices) {
+            int index = streamState.getIndex(device);
+            if (index > safeMediaVolumeIndex(device)) {
+                streamState.setIndex(safeMediaVolumeIndex(device), device, caller);
+                sendMsg(mAudioHandler,
+                        MSG_SET_DEVICE_VOLUME,
+                        SENDMSG_QUEUE,
+                        device,
+                        0,
+                        streamState,
+                        0);
+            }
+        }
+    }
+
+    private boolean checkSafeMediaVolume(int streamType, int index, int device) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            if ((mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)
+                    && (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC)
+                    && (mSafeMediaVolumeDevices.contains(device))
+                    && (index > safeMediaVolumeIndex(device))) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    @Override
+    public void disableSafeMediaVolume(String callingPackage) {
+        enforceVolumeController("disable the safe media volume");
+        synchronized (mSafeMediaVolumeStateLock) {
+            setSafeMediaVolumeEnabled(false, callingPackage);
+            if (mPendingVolumeCommand != null) {
+                onSetStreamVolume(mPendingVolumeCommand.mStreamType,
+                                  mPendingVolumeCommand.mIndex,
+                                  mPendingVolumeCommand.mFlags,
+                                  mPendingVolumeCommand.mDevice,
+                                  callingPackage);
+                mPendingVolumeCommand = null;
+            }
+        }
+    }
+
+    //==========================================================================================
+    // Hdmi CEC:
+    // - System audio mode:
+    //     If Hdmi Cec's system audio mode is on, audio service should send the volume change
+    //     to HdmiControlService so that the audio receiver can handle it.
+    // - CEC sink:
+    //     OUT_HDMI becomes a "full volume device", i.e. output is always at maximum level
+    //     and volume changes won't be taken into account on this device. Volume adjustments
+    //     are transformed into key events for the HDMI playback client.
+    //==========================================================================================
+
+    @GuardedBy("mHdmiClientLock")
+    private void updateHdmiCecSinkLocked(boolean hdmiCecSink) {
+        mHdmiCecSink = hdmiCecSink;
+        if (mHdmiCecSink) {
+            if (DEBUG_VOL) {
+                Log.d(TAG, "CEC sink: setting HDMI as full vol device");
+            }
+            mFullVolumeDevices.add(AudioSystem.DEVICE_OUT_HDMI);
+        } else {
+            if (DEBUG_VOL) {
+                Log.d(TAG, "TV, no CEC: setting HDMI as regular vol device");
+            }
+            // Android TV devices without CEC service apply software volume on
+            // HDMI output
+            mFullVolumeDevices.remove(AudioSystem.DEVICE_OUT_HDMI);
+        }
+
+        checkAddAllFixedVolumeDevices(AudioSystem.DEVICE_OUT_HDMI,
+                "HdmiPlaybackClient.DisplayStatusCallback");
+    }
+
+    private class MyHdmiControlStatusChangeListenerCallback
+            implements HdmiControlManager.HdmiControlStatusChangeListener {
+        public void onStatusChange(boolean isCecEnabled, boolean isCecAvailable) {
+            synchronized (mHdmiClientLock) {
+                if (mHdmiManager == null) return;
+                updateHdmiCecSinkLocked(isCecEnabled ? isCecAvailable : false);
+            }
+        }
+    };
+
+    private final Object mHdmiClientLock = new Object();
+
+    // If HDMI-CEC system audio is supported
+    private boolean mHdmiSystemAudioSupported = false;
+    // Set only when device is tv.
+    @GuardedBy("mHdmiClientLock")
+    private HdmiTvClient mHdmiTvClient;
+    // true if the device has system feature PackageManager.FEATURE_LEANBACK.
+    // cached HdmiControlManager interface
+    @GuardedBy("mHdmiClientLock")
+    private HdmiControlManager mHdmiManager;
+    // Set only when device is a set-top box.
+    @GuardedBy("mHdmiClientLock")
+    private HdmiPlaybackClient mHdmiPlaybackClient;
+    // true if we are a set-top box, an HDMI sink is connected and it supports CEC.
+    @GuardedBy("mHdmiClientLock")
+    private boolean mHdmiCecSink;
+    // Set only when device is an audio system.
+    @GuardedBy("mHdmiClientLock")
+    private HdmiAudioSystemClient mHdmiAudioSystemClient;
+
+    private MyHdmiControlStatusChangeListenerCallback mHdmiControlStatusChangeListenerCallback =
+            new MyHdmiControlStatusChangeListenerCallback();
+
+    @Override
+    public int setHdmiSystemAudioSupported(boolean on) {
+        int device = AudioSystem.DEVICE_NONE;
+        synchronized (mHdmiClientLock) {
+            if (mHdmiManager != null) {
+                if (mHdmiTvClient == null && mHdmiAudioSystemClient == null) {
+                    Log.w(TAG, "Only Hdmi-Cec enabled TV or audio system device supports"
+                            + "system audio mode.");
+                    return device;
+                }
+                if (mHdmiSystemAudioSupported != on) {
+                    mHdmiSystemAudioSupported = on;
+                    final int config = on ? AudioSystem.FORCE_HDMI_SYSTEM_AUDIO_ENFORCED :
+                        AudioSystem.FORCE_NONE;
+                    mDeviceBroker.setForceUse_Async(AudioSystem.FOR_HDMI_SYSTEM_AUDIO, config,
+                            "setHdmiSystemAudioSupported");
+                }
+                device = getDevicesForStream(AudioSystem.STREAM_MUSIC);
+            }
+        }
+        return device;
+    }
+
+    @Override
+    public boolean isHdmiSystemAudioSupported() {
+        return mHdmiSystemAudioSupported;
+    }
+
+    //==========================================================================================
+    // Accessibility
+
+    private void initA11yMonitoring() {
+        final AccessibilityManager accessibilityManager =
+                (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+        updateDefaultStreamOverrideDelay(accessibilityManager.isTouchExplorationEnabled());
+        updateA11yVolumeAlias(accessibilityManager.isAccessibilityVolumeStreamActive());
+        accessibilityManager.addTouchExplorationStateChangeListener(this, null);
+        accessibilityManager.addAccessibilityServicesStateChangeListener(this, null);
+    }
+
+    //---------------------------------------------------------------------------------
+    // A11y: taking touch exploration into account for selecting the default
+    //   stream override timeout when adjusting volume
+    //---------------------------------------------------------------------------------
+
+    // - STREAM_NOTIFICATION on tablets during this period after a notification stopped
+    // - STREAM_RING on phones during this period after a notification stopped
+    // - STREAM_MUSIC otherwise
+
+    private static final int DEFAULT_STREAM_TYPE_OVERRIDE_DELAY_MS = 0;
+    private static final int TOUCH_EXPLORE_STREAM_TYPE_OVERRIDE_DELAY_MS = 1000;
+
+    private static int sStreamOverrideDelayMs;
+
+    @Override
+    public void onTouchExplorationStateChanged(boolean enabled) {
+        updateDefaultStreamOverrideDelay(enabled);
+    }
+
+    private void updateDefaultStreamOverrideDelay(boolean touchExploreEnabled) {
+        if (touchExploreEnabled) {
+            sStreamOverrideDelayMs = TOUCH_EXPLORE_STREAM_TYPE_OVERRIDE_DELAY_MS;
+        } else {
+            sStreamOverrideDelayMs = DEFAULT_STREAM_TYPE_OVERRIDE_DELAY_MS;
+        }
+        if (DEBUG_VOL) Log.d(TAG, "Touch exploration enabled=" + touchExploreEnabled
+                + " stream override delay is now " + sStreamOverrideDelayMs + " ms");
+    }
+
+    //---------------------------------------------------------------------------------
+    // A11y: taking a11y state into account for the handling of a11y prompts volume
+    //---------------------------------------------------------------------------------
+
+    private static boolean sIndependentA11yVolume = false;
+
+    // implementation of AccessibilityServicesStateChangeListener
+    @Override
+    public void onAccessibilityServicesStateChanged(AccessibilityManager accessibilityManager) {
+        updateA11yVolumeAlias(accessibilityManager.isAccessibilityVolumeStreamActive());
+    }
+
+    private void updateA11yVolumeAlias(boolean a11VolEnabled) {
+        if (DEBUG_VOL) Log.d(TAG, "Accessibility volume enabled = " + a11VolEnabled);
+        if (sIndependentA11yVolume != a11VolEnabled) {
+            sIndependentA11yVolume = a11VolEnabled;
+            // update the volume mapping scheme
+            updateStreamVolumeAlias(true /*updateVolumes*/, TAG);
+            // update the volume controller behavior
+            mVolumeController.setA11yMode(sIndependentA11yVolume ?
+                    VolumePolicy.A11Y_MODE_INDEPENDENT_A11Y_VOLUME :
+                        VolumePolicy.A11Y_MODE_MEDIA_A11Y_VOLUME);
+            mVolumeController.postVolumeChanged(AudioManager.STREAM_ACCESSIBILITY, 0);
+        }
+    }
+
+    //==========================================================================================
+    // Camera shutter sound policy.
+    // config_camera_sound_forced configuration option in config.xml defines if the camera shutter
+    // sound is forced (sound even if the device is in silent mode) or not. This option is false by
+    // default and can be overridden by country specific overlay in values-mccXXX/config.xml.
+    //==========================================================================================
+
+    // cached value of com.android.internal.R.bool.config_camera_sound_forced
+    @GuardedBy("mSettingsLock")
+    private boolean mCameraSoundForced;
+
+    // called by android.hardware.Camera to populate CameraInfo.canDisableShutterSound
+    public boolean isCameraSoundForced() {
+        synchronized (mSettingsLock) {
+            return mCameraSoundForced;
+        }
+    }
+
+    //==========================================================================================
+    // AudioService logging and dumpsys
+    //==========================================================================================
+    static final int LOG_NB_EVENTS_PHONE_STATE = 20;
+    static final int LOG_NB_EVENTS_DEVICE_CONNECTION = 30;
+    static final int LOG_NB_EVENTS_FORCE_USE = 20;
+    static final int LOG_NB_EVENTS_VOLUME = 40;
+    static final int LOG_NB_EVENTS_DYN_POLICY = 10;
+
+    final private AudioEventLogger mModeLogger = new AudioEventLogger(LOG_NB_EVENTS_PHONE_STATE,
+            "phone state (logged after successful call to AudioSystem.setPhoneState(int, int))");
+
+    // logs for wired + A2DP device connections:
+    // - wired: logged before onSetWiredDeviceConnectionState() is executed
+    // - A2DP: logged at reception of method call
+    /*package*/ static final AudioEventLogger sDeviceLogger = new AudioEventLogger(
+            LOG_NB_EVENTS_DEVICE_CONNECTION, "wired/A2DP/hearing aid device connection");
+
+    static final AudioEventLogger sForceUseLogger = new AudioEventLogger(
+            LOG_NB_EVENTS_FORCE_USE,
+            "force use (logged before setForceUse() is executed)");
+
+    static final AudioEventLogger sVolumeLogger = new AudioEventLogger(LOG_NB_EVENTS_VOLUME,
+            "volume changes (logged when command received by AudioService)");
+
+    final private AudioEventLogger mDynPolicyLogger = new AudioEventLogger(LOG_NB_EVENTS_DYN_POLICY,
+            "dynamic policy events (logged when command received by AudioService)");
+
+    private static final String[] RINGER_MODE_NAMES = new String[] {
+            "SILENT",
+            "VIBRATE",
+            "NORMAL"
+    };
+
+    private void dumpRingerMode(PrintWriter pw) {
+        pw.println("\nRinger mode: ");
+        pw.println("- mode (internal) = " + RINGER_MODE_NAMES[mRingerMode]);
+        pw.println("- mode (external) = " + RINGER_MODE_NAMES[mRingerModeExternal]);
+        pw.println("- zen mode:" + Settings.Global.zenModeToString(mNm.getZenMode()));
+        dumpRingerModeStreams(pw, "affected", mRingerModeAffectedStreams);
+        dumpRingerModeStreams(pw, "muted", mRingerAndZenModeMutedStreams);
+        pw.print("- delegate = "); pw.println(mRingerModeDelegate);
+    }
+
+    private void dumpRingerModeStreams(PrintWriter pw, String type, int streams) {
+        pw.print("- ringer mode "); pw.print(type); pw.print(" streams = 0x");
+        pw.print(Integer.toHexString(streams));
+        if (streams != 0) {
+            pw.print(" (");
+            boolean first = true;
+            for (int i = 0; i < AudioSystem.STREAM_NAMES.length; i++) {
+                final int stream = (1 << i);
+                if ((streams & stream) != 0) {
+                    if (!first) pw.print(',');
+                    pw.print(AudioSystem.STREAM_NAMES[i]);
+                    streams &= ~stream;
+                    first = false;
+                }
+            }
+            if (streams != 0) {
+                if (!first) pw.print(',');
+                pw.print(streams);
+            }
+            pw.print(')');
+        }
+        pw.println();
+    }
+
+    private String dumpDeviceTypes(@NonNull Set<Integer> deviceTypes) {
+        Iterator<Integer> it = deviceTypes.iterator();
+        if (!it.hasNext()) {
+            return "";
+        }
+        final StringBuilder sb = new StringBuilder();
+        sb.append("0x" + Integer.toHexString(it.next()));
+        while (it.hasNext()) {
+            sb.append("," + "0x" + Integer.toHexString(it.next()));
+        }
+        return sb.toString();
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+        if (mAudioHandler != null) {
+            pw.println("\nMessage handler (watch for unhandled messages):");
+            mAudioHandler.dump(new PrintWriterPrinter(pw), "  ");
+        } else {
+            pw.println("\nMessage handler is null");
+        }
+        mMediaFocusControl.dump(pw);
+        dumpStreamStates(pw);
+        dumpVolumeGroups(pw);
+        dumpRingerMode(pw);
+        pw.println("\nAudio routes:");
+        pw.print("  mMainType=0x"); pw.println(Integer.toHexString(
+                mDeviceBroker.getCurAudioRoutes().mainType));
+        pw.print("  mBluetoothName="); pw.println(mDeviceBroker.getCurAudioRoutes().bluetoothName);
+
+        pw.println("\nOther state:");
+        pw.print("  mVolumeController="); pw.println(mVolumeController);
+        pw.print("  mSafeMediaVolumeState=");
+        pw.println(safeMediaVolumeStateToString(mSafeMediaVolumeState));
+        pw.print("  mSafeMediaVolumeIndex="); pw.println(mSafeMediaVolumeIndex);
+        pw.print("  mSafeUsbMediaVolumeIndex="); pw.println(mSafeUsbMediaVolumeIndex);
+        pw.print("  mSafeUsbMediaVolumeDbfs="); pw.println(mSafeUsbMediaVolumeDbfs);
+        pw.print("  sIndependentA11yVolume="); pw.println(sIndependentA11yVolume);
+        pw.print("  mPendingVolumeCommand="); pw.println(mPendingVolumeCommand);
+        pw.print("  mMusicActiveMs="); pw.println(mMusicActiveMs);
+        pw.print("  mMcc="); pw.println(mMcc);
+        pw.print("  mCameraSoundForced="); pw.println(mCameraSoundForced);
+        pw.print("  mHasVibrator="); pw.println(mHasVibrator);
+        pw.print("  mVolumePolicy="); pw.println(mVolumePolicy);
+        pw.print("  mAvrcpAbsVolSupported=");
+        pw.println(mDeviceBroker.isAvrcpAbsoluteVolumeSupported());
+        pw.print("  mIsSingleVolume="); pw.println(mIsSingleVolume);
+        pw.print("  mUseFixedVolume="); pw.println(mUseFixedVolume);
+        pw.print("  mFixedVolumeDevices="); pw.println(dumpDeviceTypes(mFixedVolumeDevices));
+        pw.print("  mHdmiCecSink="); pw.println(mHdmiCecSink);
+        pw.print("  mHdmiAudioSystemClient="); pw.println(mHdmiAudioSystemClient);
+        pw.print("  mHdmiPlaybackClient="); pw.println(mHdmiPlaybackClient);
+        pw.print("  mHdmiTvClient="); pw.println(mHdmiTvClient);
+        pw.print("  mHdmiSystemAudioSupported="); pw.println(mHdmiSystemAudioSupported);
+        pw.print("  mIsCallScreeningModeSupported="); pw.println(mIsCallScreeningModeSupported);
+        pw.print("  mic mute FromSwitch=" + mMicMuteFromSwitch
+                        + " FromRestrictions=" + mMicMuteFromRestrictions
+                        + " FromApi=" + mMicMuteFromApi
+                        + " from system=" + mMicMuteFromSystemCached);
+
+        dumpAudioPolicies(pw);
+        mDynPolicyLogger.dump(pw);
+        mPlaybackMonitor.dump(pw);
+        mRecordMonitor.dump(pw);
+
+        pw.println("\nAudioDeviceBroker:");
+        mDeviceBroker.dump(pw, "  ");
+        pw.println("\nSoundEffects:");
+        mSfxHelper.dump(pw, "  ");
+
+        pw.println("\n");
+        pw.println("\nEvent logs:");
+        mModeLogger.dump(pw);
+        pw.println("\n");
+        sDeviceLogger.dump(pw);
+        pw.println("\n");
+        sForceUseLogger.dump(pw);
+        pw.println("\n");
+        sVolumeLogger.dump(pw);
+        pw.println("\n");
+        dumpSupportedSystemUsage(pw);
+    }
+
+    private void dumpSupportedSystemUsage(PrintWriter pw) {
+        pw.println("Supported System Usages:");
+        synchronized (mSupportedSystemUsagesLock) {
+            for (int i = 0; i < mSupportedSystemUsages.length; i++) {
+                pw.printf("\t%s\n", AudioAttributes.usageToString(mSupportedSystemUsages[i]));
+            }
+        }
+    }
+
+    /**
+     * Audio Analytics ids.
+     */
+    private static final String mMetricsId = MediaMetrics.Name.AUDIO_SERVICE
+            + MediaMetrics.SEPARATOR;
+
+    private static String safeMediaVolumeStateToString(int state) {
+        switch(state) {
+            case SAFE_MEDIA_VOLUME_NOT_CONFIGURED: return "SAFE_MEDIA_VOLUME_NOT_CONFIGURED";
+            case SAFE_MEDIA_VOLUME_DISABLED: return "SAFE_MEDIA_VOLUME_DISABLED";
+            case SAFE_MEDIA_VOLUME_INACTIVE: return "SAFE_MEDIA_VOLUME_INACTIVE";
+            case SAFE_MEDIA_VOLUME_ACTIVE: return "SAFE_MEDIA_VOLUME_ACTIVE";
+        }
+        return null;
+    }
+
+    // Inform AudioFlinger of our device's low RAM attribute
+    private static void readAndSetLowRamDevice()
+    {
+        boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic();
+        long totalMemory = 1024 * 1024 * 1024; // 1GB is the default if ActivityManager fails.
+
+        try {
+            final ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
+            ActivityManager.getService().getMemoryInfo(info);
+            totalMemory = info.totalMem;
+        } catch (RemoteException e) {
+            Log.w(TAG, "Cannot obtain MemoryInfo from ActivityManager, assume low memory device");
+            isLowRamDevice = true;
+        }
+
+        final int status = AudioSystem.setLowRamDevice(isLowRamDevice, totalMemory);
+        if (status != 0) {
+            Log.w(TAG, "AudioFlinger informed of device's low RAM attribute; status " + status);
+        }
+    }
+
+    private void enforceVolumeController(String action) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.STATUS_BAR_SERVICE,
+                "Only SystemUI can " + action);
+    }
+
+    @Override
+    public void setVolumeController(final IVolumeController controller) {
+        enforceVolumeController("set the volume controller");
+
+        // return early if things are not actually changing
+        if (mVolumeController.isSameBinder(controller)) {
+            return;
+        }
+
+        // dismiss the old volume controller
+        mVolumeController.postDismiss();
+        if (controller != null) {
+            // we are about to register a new controller, listen for its death
+            try {
+                controller.asBinder().linkToDeath(new DeathRecipient() {
+                    @Override
+                    public void binderDied() {
+                        if (mVolumeController.isSameBinder(controller)) {
+                            Log.w(TAG, "Current remote volume controller died, unregistering");
+                            setVolumeController(null);
+                        }
+                    }
+                }, 0);
+            } catch (RemoteException e) {
+                // noop
+            }
+        }
+        mVolumeController.setController(controller);
+        if (DEBUG_VOL) Log.d(TAG, "Volume controller: " + mVolumeController);
+    }
+
+    @Override
+    public void notifyVolumeControllerVisible(final IVolumeController controller, boolean visible) {
+        enforceVolumeController("notify about volume controller visibility");
+
+        // return early if the controller is not current
+        if (!mVolumeController.isSameBinder(controller)) {
+            return;
+        }
+
+        mVolumeController.setVisible(visible);
+        if (DEBUG_VOL) Log.d(TAG, "Volume controller visible: " + visible);
+    }
+
+    @Override
+    public void setVolumePolicy(VolumePolicy policy) {
+        enforceVolumeController("set volume policy");
+        if (policy != null && !policy.equals(mVolumePolicy)) {
+            mVolumePolicy = policy;
+            if (DEBUG_VOL) Log.d(TAG, "Volume policy changed: " + mVolumePolicy);
+        }
+    }
+
+    public static class VolumeController {
+        private static final String TAG = "VolumeController";
+
+        private IVolumeController mController;
+        private boolean mVisible;
+        private long mNextLongPress;
+        private int mLongPressTimeout;
+
+        public void setController(IVolumeController controller) {
+            mController = controller;
+            mVisible = false;
+        }
+
+        public void loadSettings(ContentResolver cr) {
+            mLongPressTimeout = Settings.Secure.getIntForUser(cr,
+                    Settings.Secure.LONG_PRESS_TIMEOUT, 500, UserHandle.USER_CURRENT);
+        }
+
+        public boolean suppressAdjustment(int resolvedStream, int flags, boolean isMute) {
+            if (isMute) {
+                return false;
+            }
+            boolean suppress = false;
+            if (resolvedStream != AudioSystem.STREAM_MUSIC && mController != null) {
+                final long now = SystemClock.uptimeMillis();
+                if ((flags & AudioManager.FLAG_SHOW_UI) != 0 && !mVisible) {
+                    // ui will become visible
+                    if (mNextLongPress < now) {
+                        mNextLongPress = now + mLongPressTimeout;
+                    }
+                    suppress = true;
+                } else if (mNextLongPress > 0) {  // in a long-press
+                    if (now > mNextLongPress) {
+                        // long press triggered, no more suppression
+                        mNextLongPress = 0;
+                    } else {
+                        // keep suppressing until the long press triggers
+                        suppress = true;
+                    }
+                }
+            }
+            return suppress;
+        }
+
+        public void setVisible(boolean visible) {
+            mVisible = visible;
+        }
+
+        public boolean isSameBinder(IVolumeController controller) {
+            return Objects.equals(asBinder(), binder(controller));
+        }
+
+        public IBinder asBinder() {
+            return binder(mController);
+        }
+
+        private static IBinder binder(IVolumeController controller) {
+            return controller == null ? null : controller.asBinder();
+        }
+
+        @Override
+        public String toString() {
+            return "VolumeController(" + asBinder() + ",mVisible=" + mVisible + ")";
+        }
+
+        public void postDisplaySafeVolumeWarning(int flags) {
+            if (mController == null)
+                return;
+            try {
+                mController.displaySafeVolumeWarning(flags);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling displaySafeVolumeWarning", e);
+            }
+        }
+
+        public void postVolumeChanged(int streamType, int flags) {
+            if (mController == null)
+                return;
+            try {
+                mController.volumeChanged(streamType, flags);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling volumeChanged", e);
+            }
+        }
+
+        public void postMasterMuteChanged(int flags) {
+            if (mController == null)
+                return;
+            try {
+                mController.masterMuteChanged(flags);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling masterMuteChanged", e);
+            }
+        }
+
+        public void setLayoutDirection(int layoutDirection) {
+            if (mController == null)
+                return;
+            try {
+                mController.setLayoutDirection(layoutDirection);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling setLayoutDirection", e);
+            }
+        }
+
+        public void postDismiss() {
+            if (mController == null)
+                return;
+            try {
+                mController.dismiss();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling dismiss", e);
+            }
+        }
+
+        public void setA11yMode(int a11yMode) {
+            if (mController == null)
+                return;
+            try {
+                mController.setA11yMode(a11yMode);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Error calling setA11Mode", e);
+            }
+        }
+    }
+
+    /**
+     * Interface for system components to get some extra functionality through
+     * LocalServices.
+     */
+    final class AudioServiceInternal extends AudioManagerInternal {
+        @Override
+        public void setRingerModeDelegate(RingerModeDelegate delegate) {
+            mRingerModeDelegate = delegate;
+            if (mRingerModeDelegate != null) {
+                synchronized (mSettingsLock) {
+                    updateRingerAndZenModeAffectedStreams();
+                }
+                setRingerModeInternal(getRingerModeInternal(), TAG + ".setRingerModeDelegate");
+            }
+        }
+
+        @Override
+        public void adjustSuggestedStreamVolumeForUid(int streamType, int direction, int flags,
+                String callingPackage, int uid) {
+            // direction and stream type swap here because the public
+            // adjustSuggested has a different order than the other methods.
+            adjustSuggestedStreamVolume(direction, streamType, flags, callingPackage,
+                    callingPackage, uid);
+        }
+
+        @Override
+        public void adjustStreamVolumeForUid(int streamType, int direction, int flags,
+                String callingPackage, int uid) {
+            if (direction != AudioManager.ADJUST_SAME) {
+                sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_VOL_UID, streamType,
+                        direction/*val1*/, flags/*val2*/, new StringBuilder(callingPackage)
+                        .append(" uid:").append(uid).toString()));
+            }
+            adjustStreamVolume(streamType, direction, flags, callingPackage,
+                    callingPackage, uid);
+        }
+
+        @Override
+        public void setStreamVolumeForUid(int streamType, int direction, int flags,
+                String callingPackage, int uid) {
+            setStreamVolume(streamType, direction, flags, callingPackage, callingPackage, uid);
+        }
+
+        @Override
+        public int getRingerModeInternal() {
+            return AudioService.this.getRingerModeInternal();
+        }
+
+        @Override
+        public void setRingerModeInternal(int ringerMode, String caller) {
+            AudioService.this.setRingerModeInternal(ringerMode, caller);
+        }
+
+        @Override
+        public void silenceRingerModeInternal(String caller) {
+            AudioService.this.silenceRingerModeInternal(caller);
+        }
+
+        @Override
+        public void updateRingerModeAffectedStreamsInternal() {
+            synchronized (mSettingsLock) {
+                if (updateRingerAndZenModeAffectedStreams()) {
+                    setRingerModeInt(getRingerModeInternal(), false);
+                }
+            }
+        }
+
+        @Override
+        public void setAccessibilityServiceUids(IntArray uids) {
+            synchronized (mAccessibilityServiceUidsLock) {
+                if (uids.size() == 0) {
+                    mAccessibilityServiceUids = null;
+                } else {
+                    boolean changed = (mAccessibilityServiceUids == null)
+                            || (mAccessibilityServiceUids.length != uids.size());
+                    if (!changed) {
+                        for (int i = 0; i < mAccessibilityServiceUids.length; i++) {
+                            if (uids.get(i) != mAccessibilityServiceUids[i]) {
+                                changed = true;
+                                break;
+                            }
+                        }
+                    }
+                    if (changed) {
+                        mAccessibilityServiceUids = uids.toArray();
+                    }
+                }
+                AudioSystem.setA11yServicesUids(mAccessibilityServiceUids);
+            }
+        }
+    }
+
+    //==========================================================================================
+    // Audio policy management
+    //==========================================================================================
+    public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb,
+            boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
+            boolean isVolumeController, IMediaProjection projection) {
+        AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback);
+
+        if (!isPolicyRegisterAllowed(policyConfig,
+                                     isFocusPolicy || isTestFocusPolicy || hasFocusListener,
+                                     isVolumeController,
+                                     projection)) {
+            Slog.w(TAG, "Permission denied to register audio policy for pid "
+                    + Binder.getCallingPid() + " / uid " + Binder.getCallingUid()
+                    + ", need MODIFY_AUDIO_ROUTING or MediaProjection that can project audio");
+            return null;
+        }
+
+        mDynPolicyLogger.log((new AudioEventLogger.StringEvent("registerAudioPolicy for "
+                + pcb.asBinder() + " with config:" + policyConfig)).printLog(TAG));
+
+        String regId = null;
+        synchronized (mAudioPolicies) {
+            if (mAudioPolicies.containsKey(pcb.asBinder())) {
+                Slog.e(TAG, "Cannot re-register policy");
+                return null;
+            }
+            try {
+                AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, pcb, hasFocusListener,
+                        isFocusPolicy, isTestFocusPolicy, isVolumeController, projection);
+                pcb.asBinder().linkToDeath(app, 0/*flags*/);
+                regId = app.getRegistrationId();
+                mAudioPolicies.put(pcb.asBinder(), app);
+            } catch (RemoteException e) {
+                // audio policy owner has already died!
+                Slog.w(TAG, "Audio policy registration failed, could not link to " + pcb +
+                        " binder death", e);
+                return null;
+            } catch (IllegalStateException e) {
+                Slog.w(TAG, "Audio policy registration failed for binder " + pcb, e);
+                return null;
+            }
+        }
+        return regId;
+    }
+
+    /**
+     * Apps with MODIFY_AUDIO_ROUTING can register any policy.
+     * Apps with an audio capable MediaProjection are allowed to register a RENDER|LOOPBACK policy
+     * as those policy do not modify the audio routing.
+     */
+    private boolean isPolicyRegisterAllowed(AudioPolicyConfig policyConfig,
+                                            boolean hasFocusAccess,
+                                            boolean isVolumeController,
+                                            IMediaProjection projection) {
+
+        boolean requireValidProjection = false;
+        boolean requireCaptureAudioOrMediaOutputPerm = false;
+        boolean requireModifyRouting = false;
+        ArrayList<AudioMix> voiceCommunicationCaptureMixes = null;
+
+
+        if (hasFocusAccess || isVolumeController) {
+            requireModifyRouting |= true;
+        } else if (policyConfig.getMixes().isEmpty()) {
+            // An empty policy could be used to lock the focus or add mixes later
+            requireModifyRouting |= true;
+        }
+        for (AudioMix mix : policyConfig.getMixes()) {
+            // If mix is requesting privileged capture
+            if (mix.getRule().allowPrivilegedPlaybackCapture()) {
+                // then it must have CAPTURE_MEDIA_OUTPUT or CAPTURE_AUDIO_OUTPUT permission
+                requireCaptureAudioOrMediaOutputPerm |= true;
+
+                // and its format must be low quality enough
+                String error = mix.canBeUsedForPrivilegedCapture(mix.getFormat());
+                if (error != null) {
+                    Log.e(TAG, error);
+                    return false;
+                }
+
+                // If mix is trying to excplicitly capture USAGE_VOICE_COMMUNICATION
+                if (mix.containsMatchAttributeRuleForUsage(
+                        AudioAttributes.USAGE_VOICE_COMMUNICATION)) {
+                    // then it must have CAPTURE_USAGE_VOICE_COMMUNICATION_OUTPUT permission
+                    // Note that for UID, USERID or EXCLDUE rules, the capture will be silenced
+                    // in AudioPolicyMix
+                    if (voiceCommunicationCaptureMixes == null) {
+                        voiceCommunicationCaptureMixes = new ArrayList<AudioMix>();
+                    }
+                    voiceCommunicationCaptureMixes.add(mix);
+                }
+            }
+
+            // If mix is RENDER|LOOPBACK, then an audio MediaProjection is enough
+            // otherwise MODIFY_AUDIO_ROUTING permission is required
+            if (mix.getRouteFlags() == mix.ROUTE_FLAG_LOOP_BACK_RENDER && projection != null) {
+                requireValidProjection |= true;
+            } else {
+                requireModifyRouting |= true;
+            }
+        }
+
+        if (requireCaptureAudioOrMediaOutputPerm
+                && !callerHasPermission(android.Manifest.permission.CAPTURE_MEDIA_OUTPUT)
+                && !callerHasPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT)) {
+            Log.e(TAG, "Privileged audio capture requires CAPTURE_MEDIA_OUTPUT or "
+                      + "CAPTURE_AUDIO_OUTPUT system permission");
+            return false;
+        }
+
+        if (voiceCommunicationCaptureMixes != null && voiceCommunicationCaptureMixes.size() > 0) {
+            if (!callerHasPermission(
+                    android.Manifest.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT)) {
+                Log.e(TAG, "Privileged audio capture for voice communication requires "
+                        + "CAPTURE_VOICE_COMMUNICATION_OUTPUT system permission");
+                return false;
+            }
+
+            // If permission check succeeded, we set the flag in each of the mixing rules
+            for (AudioMix mix : voiceCommunicationCaptureMixes) {
+                mix.getRule().setVoiceCommunicationCaptureAllowed(true);
+            }
+        }
+
+        if (requireValidProjection && !canProjectAudio(projection)) {
+            return false;
+        }
+
+        if (requireModifyRouting
+                && !callerHasPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)) {
+            Log.e(TAG, "Can not capture audio without MODIFY_AUDIO_ROUTING");
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean callerHasPermission(String permission) {
+        return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /** @return true if projection is a valid MediaProjection that can project audio. */
+    private boolean canProjectAudio(IMediaProjection projection) {
+        if (projection == null) {
+            Log.e(TAG, "MediaProjection is null");
+            return false;
+        }
+
+        IMediaProjectionManager projectionService = getProjectionService();
+        if (projectionService == null) {
+            Log.e(TAG, "Can't get service IMediaProjectionManager");
+            return false;
+        }
+
+        try {
+            if (!projectionService.isValidMediaProjection(projection)) {
+                Log.w(TAG, "App passed invalid MediaProjection token");
+                return false;
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Can't call .isValidMediaProjection() on IMediaProjectionManager"
+                    + projectionService.asBinder(), e);
+            return false;
+        }
+
+        try {
+            if (!projection.canProjectAudio()) {
+                Log.w(TAG, "App passed MediaProjection that can not project audio");
+                return false;
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Can't call .canProjectAudio() on valid IMediaProjection"
+                    + projection.asBinder(), e);
+            return false;
+        }
+
+        return true;
+    }
+
+    private IMediaProjectionManager getProjectionService() {
+        if (mProjectionService == null) {
+            IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE);
+            mProjectionService = IMediaProjectionManager.Stub.asInterface(b);
+        }
+        return mProjectionService;
+    }
+
+    /**
+     * See {@link AudioManager#unregisterAudioPolicyAsync(AudioPolicy)}
+     * Declared oneway
+     * @param pcb nullable because on service interface
+     */
+    public void unregisterAudioPolicyAsync(@Nullable IAudioPolicyCallback pcb) {
+        unregisterAudioPolicy(pcb);
+    }
+
+    /**
+     * See {@link AudioManager#unregisterAudioPolicy(AudioPolicy)}
+     * @param pcb nullable because on service interface
+     */
+    public void unregisterAudioPolicy(@Nullable IAudioPolicyCallback pcb) {
+        if (pcb == null) {
+            return;
+        }
+        unregisterAudioPolicyInt(pcb);
+    }
+
+
+    private void unregisterAudioPolicyInt(@NonNull IAudioPolicyCallback pcb) {
+        mDynPolicyLogger.log((new AudioEventLogger.StringEvent("unregisterAudioPolicyAsync for "
+                + pcb.asBinder()).printLog(TAG)));
+        synchronized (mAudioPolicies) {
+            AudioPolicyProxy app = mAudioPolicies.remove(pcb.asBinder());
+            if (app == null) {
+                Slog.w(TAG, "Trying to unregister unknown audio policy for pid "
+                        + Binder.getCallingPid() + " / uid " + Binder.getCallingUid());
+                return;
+            } else {
+                pcb.asBinder().unlinkToDeath(app, 0/*flags*/);
+            }
+            app.release();
+        }
+        // TODO implement clearing mix attribute matching info in native audio policy
+    }
+
+    /**
+     * Checks whether caller has MODIFY_AUDIO_ROUTING permission, and the policy is registered.
+     * @param errorMsg log warning if permission check failed.
+     * @return null if the operation on the audio mixes should be cancelled.
+     */
+    @GuardedBy("mAudioPolicies")
+    private AudioPolicyProxy checkUpdateForPolicy(IAudioPolicyCallback pcb, String errorMsg) {
+        // permission check
+        final boolean hasPermissionForPolicy =
+                (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
+                        android.Manifest.permission.MODIFY_AUDIO_ROUTING));
+        if (!hasPermissionForPolicy) {
+            Slog.w(TAG, errorMsg + " for pid " +
+                    + Binder.getCallingPid() + " / uid "
+                    + Binder.getCallingUid() + ", need MODIFY_AUDIO_ROUTING");
+            return null;
+        }
+        // policy registered?
+        final AudioPolicyProxy app = mAudioPolicies.get(pcb.asBinder());
+        if (app == null) {
+            Slog.w(TAG, errorMsg + " for pid " +
+                    + Binder.getCallingPid() + " / uid "
+                    + Binder.getCallingUid() + ", unregistered policy");
+            return null;
+        }
+        return app;
+    }
+
+    public int addMixForPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb) {
+        if (DEBUG_AP) { Log.d(TAG, "addMixForPolicy for " + pcb.asBinder()
+                + " with config:" + policyConfig); }
+        synchronized (mAudioPolicies) {
+            final AudioPolicyProxy app =
+                    checkUpdateForPolicy(pcb, "Cannot add AudioMix in audio policy");
+            if (app == null){
+                return AudioManager.ERROR;
+            }
+            return app.addMixes(policyConfig.getMixes()) == AudioSystem.SUCCESS
+                ? AudioManager.SUCCESS : AudioManager.ERROR;
+        }
+    }
+
+    public int removeMixForPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb) {
+        if (DEBUG_AP) { Log.d(TAG, "removeMixForPolicy for " + pcb.asBinder()
+                + " with config:" + policyConfig); }
+        synchronized (mAudioPolicies) {
+            final AudioPolicyProxy app =
+                    checkUpdateForPolicy(pcb, "Cannot add AudioMix in audio policy");
+            if (app == null) {
+                return AudioManager.ERROR;
+            }
+            return app.removeMixes(policyConfig.getMixes()) == AudioSystem.SUCCESS
+                ? AudioManager.SUCCESS : AudioManager.ERROR;
+        }
+    }
+
+    /** see AudioPolicy.setUidDeviceAffinity() */
+    public int setUidDeviceAffinity(IAudioPolicyCallback pcb, int uid,
+            @NonNull int[] deviceTypes, @NonNull String[] deviceAddresses) {
+        if (DEBUG_AP) {
+            Log.d(TAG, "setUidDeviceAffinity for " + pcb.asBinder() + " uid:" + uid);
+        }
+        synchronized (mAudioPolicies) {
+            final AudioPolicyProxy app =
+                    checkUpdateForPolicy(pcb, "Cannot change device affinity in audio policy");
+            if (app == null) {
+                return AudioManager.ERROR;
+            }
+            if (!app.hasMixRoutedToDevices(deviceTypes, deviceAddresses)) {
+                return AudioManager.ERROR;
+            }
+            return app.setUidDeviceAffinities(uid, deviceTypes, deviceAddresses);
+        }
+    }
+
+    /** see AudioPolicy.setUserIdDeviceAffinity() */
+    public int setUserIdDeviceAffinity(IAudioPolicyCallback pcb, int userId,
+            @NonNull int[] deviceTypes, @NonNull String[] deviceAddresses) {
+        if (DEBUG_AP) {
+            Log.d(TAG, "setUserIdDeviceAffinity for " + pcb.asBinder() + " user:" + userId);
+        }
+
+        synchronized (mAudioPolicies) {
+            final AudioPolicyProxy app =
+                    checkUpdateForPolicy(pcb, "Cannot change device affinity in audio policy");
+            if (app == null) {
+                return AudioManager.ERROR;
+            }
+            if (!app.hasMixRoutedToDevices(deviceTypes, deviceAddresses)) {
+                return AudioManager.ERROR;
+            }
+            return app.setUserIdDeviceAffinities(userId, deviceTypes, deviceAddresses);
+        }
+    }
+
+    /** see AudioPolicy.removeUidDeviceAffinity() */
+    public int removeUidDeviceAffinity(IAudioPolicyCallback pcb, int uid) {
+        if (DEBUG_AP) {
+            Log.d(TAG, "removeUidDeviceAffinity for " + pcb.asBinder() + " uid:" + uid);
+        }
+        synchronized (mAudioPolicies) {
+            final AudioPolicyProxy app =
+                    checkUpdateForPolicy(pcb, "Cannot remove device affinity in audio policy");
+            if (app == null) {
+                return AudioManager.ERROR;
+            }
+            return app.removeUidDeviceAffinities(uid);
+        }
+    }
+
+    /** see AudioPolicy.removeUserIdDeviceAffinity() */
+    public int removeUserIdDeviceAffinity(IAudioPolicyCallback pcb, int userId) {
+        if (DEBUG_AP) {
+            Log.d(TAG, "removeUserIdDeviceAffinity for " + pcb.asBinder()
+                    + " userId:" + userId);
+        }
+        synchronized (mAudioPolicies) {
+            final AudioPolicyProxy app =
+                    checkUpdateForPolicy(pcb, "Cannot remove device affinity in audio policy");
+            if (app == null) {
+                return AudioManager.ERROR;
+            }
+            return app.removeUserIdDeviceAffinities(userId);
+        }
+    }
+
+    public int setFocusPropertiesForPolicy(int duckingBehavior, IAudioPolicyCallback pcb) {
+        if (DEBUG_AP) Log.d(TAG, "setFocusPropertiesForPolicy() duck behavior=" + duckingBehavior
+                + " policy " +  pcb.asBinder());
+        synchronized (mAudioPolicies) {
+            final AudioPolicyProxy app =
+                    checkUpdateForPolicy(pcb, "Cannot change audio policy focus properties");
+            if (app == null){
+                return AudioManager.ERROR;
+            }
+            if (!mAudioPolicies.containsKey(pcb.asBinder())) {
+                Slog.e(TAG, "Cannot change audio policy focus properties, unregistered policy");
+                return AudioManager.ERROR;
+            }
+            if (duckingBehavior == AudioPolicy.FOCUS_POLICY_DUCKING_IN_POLICY) {
+                // is there already one policy managing ducking?
+                for (AudioPolicyProxy policy : mAudioPolicies.values()) {
+                    if (policy.mFocusDuckBehavior == AudioPolicy.FOCUS_POLICY_DUCKING_IN_POLICY) {
+                        Slog.e(TAG, "Cannot change audio policy ducking behavior, already handled");
+                        return AudioManager.ERROR;
+                    }
+                }
+            }
+            app.mFocusDuckBehavior = duckingBehavior;
+            mMediaFocusControl.setDuckingInExtPolicyAvailable(
+                    duckingBehavior == AudioPolicy.FOCUS_POLICY_DUCKING_IN_POLICY);
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /** see AudioManager.hasRegisteredDynamicPolicy */
+    public boolean hasRegisteredDynamicPolicy() {
+        synchronized (mAudioPolicies) {
+            return !mAudioPolicies.isEmpty();
+        }
+    }
+
+    private final Object mExtVolumeControllerLock = new Object();
+    private IAudioPolicyCallback mExtVolumeController;
+    private void setExtVolumeController(IAudioPolicyCallback apc) {
+        if (!mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_handleVolumeKeysInWindowManager)) {
+            Log.e(TAG, "Cannot set external volume controller: device not set for volume keys" +
+                    " handled in PhoneWindowManager");
+            return;
+        }
+        synchronized (mExtVolumeControllerLock) {
+            if (mExtVolumeController != null && !mExtVolumeController.asBinder().pingBinder()) {
+                Log.e(TAG, "Cannot set external volume controller: existing controller");
+            }
+            mExtVolumeController = apc;
+        }
+    }
+
+    private void dumpAudioPolicies(PrintWriter pw) {
+        pw.println("\nAudio policies:");
+        synchronized (mAudioPolicies) {
+            for (AudioPolicyProxy policy : mAudioPolicies.values()) {
+                pw.println(policy.toLogFriendlyString());
+            }
+        }
+    }
+
+    //======================
+    // Audio policy callbacks from AudioSystem for dynamic policies
+    //======================
+    private final AudioSystem.DynamicPolicyCallback mDynPolicyCallback =
+            new AudioSystem.DynamicPolicyCallback() {
+        public void onDynamicPolicyMixStateUpdate(String regId, int state) {
+            if (!TextUtils.isEmpty(regId)) {
+                sendMsg(mAudioHandler, MSG_DYN_POLICY_MIX_STATE_UPDATE, SENDMSG_QUEUE,
+                        state /*arg1*/, 0 /*arg2 ignored*/, regId /*obj*/, 0 /*delay*/);
+            }
+        }
+    };
+
+    private void onDynPolicyMixStateUpdate(String regId, int state) {
+        if (DEBUG_AP) Log.d(TAG, "onDynamicPolicyMixStateUpdate("+ regId + ", " + state +")");
+        synchronized (mAudioPolicies) {
+            for (AudioPolicyProxy policy : mAudioPolicies.values()) {
+                for (AudioMix mix : policy.getMixes()) {
+                    if (mix.getRegistration().equals(regId)) {
+                        try {
+                            policy.mPolicyCallback.notifyMixStateUpdate(regId, state);
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "Can't call notifyMixStateUpdate() on IAudioPolicyCallback "
+                                    + policy.mPolicyCallback.asBinder(), e);
+                        }
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
+    //======================
+    // Audio policy callbacks from AudioSystem for recording configuration updates
+    //======================
+    private final RecordingActivityMonitor mRecordMonitor;
+
+    public void registerRecordingCallback(IRecordingConfigDispatcher rcdb) {
+        final boolean isPrivileged =
+                (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
+                        android.Manifest.permission.MODIFY_AUDIO_ROUTING));
+        mRecordMonitor.registerRecordingCallback(rcdb, isPrivileged);
+    }
+
+    public void unregisterRecordingCallback(IRecordingConfigDispatcher rcdb) {
+        mRecordMonitor.unregisterRecordingCallback(rcdb);
+    }
+
+    public List<AudioRecordingConfiguration> getActiveRecordingConfigurations() {
+        final boolean isPrivileged =
+                (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
+                        android.Manifest.permission.MODIFY_AUDIO_ROUTING));
+        return mRecordMonitor.getActiveRecordingConfigurations(isPrivileged);
+    }
+
+    //======================
+    // Audio recording state notification from clients
+    //======================
+    /**
+     * Track a recorder provided by the client
+     */
+    public int trackRecorder(IBinder recorder) {
+        return mRecordMonitor.trackRecorder(recorder);
+    }
+
+    /**
+     * Receive an event from the client about a tracked recorder
+     */
+    public void recorderEvent(int riid, int event) {
+        mRecordMonitor.recorderEvent(riid, event);
+    }
+
+    /**
+     * Stop tracking the recorder
+     */
+    public void releaseRecorder(int riid) {
+        mRecordMonitor.releaseRecorder(riid);
+    }
+
+    public void disableRingtoneSync(final int userId) {
+        final int callingUserId = UserHandle.getCallingUserId();
+        if (callingUserId != userId) {
+            mContext.enforceCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                    "disable sound settings syncing for another profile");
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            // Disable the sync setting so the profile uses its own sound settings.
+            Settings.Secure.putIntForUser(mContentResolver, Settings.Secure.SYNC_PARENT_SOUNDS,
+                    0 /* false */, userId);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    //======================
+    // Audio playback notification
+    //======================
+    private final PlaybackActivityMonitor mPlaybackMonitor;
+
+    public void registerPlaybackCallback(IPlaybackConfigDispatcher pcdb) {
+        final boolean isPrivileged =
+                (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+                        android.Manifest.permission.MODIFY_AUDIO_ROUTING));
+        mPlaybackMonitor.registerPlaybackCallback(pcdb, isPrivileged);
+    }
+
+    public void unregisterPlaybackCallback(IPlaybackConfigDispatcher pcdb) {
+        mPlaybackMonitor.unregisterPlaybackCallback(pcdb);
+    }
+
+    public List<AudioPlaybackConfiguration> getActivePlaybackConfigurations() {
+        final boolean isPrivileged =
+                (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+                        android.Manifest.permission.MODIFY_AUDIO_ROUTING));
+        return mPlaybackMonitor.getActivePlaybackConfigurations(isPrivileged);
+    }
+
+    public int trackPlayer(PlayerBase.PlayerIdCard pic) {
+        if (pic != null && pic.mAttributes != null) {
+            validateAudioAttributesUsage(pic.mAttributes);
+        }
+        return mPlaybackMonitor.trackPlayer(pic);
+    }
+
+    public void playerAttributes(int piid, AudioAttributes attr) {
+        if (attr != null) {
+            validateAudioAttributesUsage(attr);
+        }
+        mPlaybackMonitor.playerAttributes(piid, attr, Binder.getCallingUid());
+    }
+
+    public void playerEvent(int piid, int event) {
+        mPlaybackMonitor.playerEvent(piid, event, Binder.getCallingUid());
+    }
+
+    public void playerHasOpPlayAudio(int piid, boolean hasOpPlayAudio) {
+        mPlaybackMonitor.playerHasOpPlayAudio(piid, hasOpPlayAudio, Binder.getCallingUid());
+    }
+
+    public void releasePlayer(int piid) {
+        mPlaybackMonitor.releasePlayer(piid, Binder.getCallingUid());
+    }
+
+    /**
+     * Specifies whether the audio played by this app may or may not be captured by other apps or
+     * the system.
+     *
+     * @param capturePolicy one of
+     *     {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL},
+     *     {@link AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM},
+     *     {@link AudioAttributes#ALLOW_CAPTURE_BY_NONE}.
+     * @return AudioSystem.AUDIO_STATUS_OK if set allowed capture policy succeed.
+     * @throws IllegalArgumentException if the argument is not a valid value.
+     */
+    public int setAllowedCapturePolicy(int capturePolicy) {
+        int callingUid = Binder.getCallingUid();
+        int flags = AudioAttributes.capturePolicyToFlags(capturePolicy, 0x0);
+        final long identity = Binder.clearCallingIdentity();
+        synchronized (mPlaybackMonitor) {
+            int result = AudioSystem.setAllowedCapturePolicy(callingUid, flags);
+            if (result == AudioSystem.AUDIO_STATUS_OK) {
+                mPlaybackMonitor.setAllowedCapturePolicy(callingUid, capturePolicy);
+            }
+            Binder.restoreCallingIdentity(identity);
+            return result;
+        }
+    }
+
+    /**
+     * Return the capture policy.
+     * @return the cached capture policy for the calling uid.
+     */
+    public int getAllowedCapturePolicy() {
+        int callingUid = Binder.getCallingUid();
+        final long identity = Binder.clearCallingIdentity();
+        int capturePolicy = mPlaybackMonitor.getAllowedCapturePolicy(callingUid);
+        Binder.restoreCallingIdentity(identity);
+        return capturePolicy;
+    }
+
+    //======================
+    // Audio device management
+    //======================
+    private final AudioDeviceBroker mDeviceBroker;
+
+    //======================
+    // Audio policy proxy
+    //======================
+    private static final class AudioDeviceArray {
+        final @NonNull int[] mDeviceTypes;
+        final @NonNull String[] mDeviceAddresses;
+        AudioDeviceArray(@NonNull int[] types,  @NonNull String[] addresses) {
+            mDeviceTypes = types;
+            mDeviceAddresses = addresses;
+        }
+    }
+
+    /**
+     * This internal class inherits from AudioPolicyConfig, each instance contains all the
+     * mixes of an AudioPolicy and their configurations.
+     */
+    public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient {
+        private static final String TAG = "AudioPolicyProxy";
+        final IAudioPolicyCallback mPolicyCallback;
+        final boolean mHasFocusListener;
+        final boolean mIsVolumeController;
+        final HashMap<Integer, AudioDeviceArray> mUidDeviceAffinities =
+                new HashMap<Integer, AudioDeviceArray>();
+
+        final HashMap<Integer, AudioDeviceArray> mUserIdDeviceAffinities =
+                new HashMap<>();
+
+        final IMediaProjection mProjection;
+        private final class UnregisterOnStopCallback extends IMediaProjectionCallback.Stub {
+            public void onStop() {
+                unregisterAudioPolicyAsync(mPolicyCallback);
+            }
+        };
+        UnregisterOnStopCallback mProjectionCallback;
+
+        /**
+         * Audio focus ducking behavior for an audio policy.
+         * This variable reflects the value that was successfully set in
+         * {@link AudioService#setFocusPropertiesForPolicy(int, IAudioPolicyCallback)}. This
+         * implies that a value of FOCUS_POLICY_DUCKING_IN_POLICY means the corresponding policy
+         * is handling ducking for audio focus.
+         */
+        int mFocusDuckBehavior = AudioPolicy.FOCUS_POLICY_DUCKING_DEFAULT;
+        boolean mIsFocusPolicy = false;
+        boolean mIsTestFocusPolicy = false;
+
+        AudioPolicyProxy(AudioPolicyConfig config, IAudioPolicyCallback token,
+                boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
+                boolean isVolumeController, IMediaProjection projection) {
+            super(config);
+            setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++));
+            mPolicyCallback = token;
+            mHasFocusListener = hasFocusListener;
+            mIsVolumeController = isVolumeController;
+            mProjection = projection;
+            if (mHasFocusListener) {
+                mMediaFocusControl.addFocusFollower(mPolicyCallback);
+                // can only ever be true if there is a focus listener
+                if (isFocusPolicy) {
+                    mIsFocusPolicy = true;
+                    mIsTestFocusPolicy = isTestFocusPolicy;
+                    mMediaFocusControl.setFocusPolicy(mPolicyCallback, mIsTestFocusPolicy);
+                }
+            }
+            if (mIsVolumeController) {
+                setExtVolumeController(mPolicyCallback);
+            }
+            if (mProjection != null) {
+                mProjectionCallback = new UnregisterOnStopCallback();
+                try {
+                    mProjection.registerCallback(mProjectionCallback);
+                } catch (RemoteException e) {
+                    release();
+                    throw new IllegalStateException("MediaProjection callback registration failed, "
+                            + "could not link to " + projection + " binder death", e);
+                }
+            }
+            int status = connectMixes();
+            if (status != AudioSystem.SUCCESS) {
+                release();
+                throw new IllegalStateException("Could not connect mix, error: " + status);
+            }
+        }
+
+        public void binderDied() {
+            Log.i(TAG, "audio policy " + mPolicyCallback + " died");
+            release();
+        }
+
+        String getRegistrationId() {
+            return getRegistration();
+        }
+
+        void release() {
+            if (mIsFocusPolicy) {
+                mMediaFocusControl.unsetFocusPolicy(mPolicyCallback, mIsTestFocusPolicy);
+            }
+            if (mFocusDuckBehavior == AudioPolicy.FOCUS_POLICY_DUCKING_IN_POLICY) {
+                mMediaFocusControl.setDuckingInExtPolicyAvailable(false);
+            }
+            if (mHasFocusListener) {
+                mMediaFocusControl.removeFocusFollower(mPolicyCallback);
+            }
+            if (mProjectionCallback != null) {
+                try {
+                    mProjection.unregisterCallback(mProjectionCallback);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Fail to unregister Audiopolicy callback from MediaProjection");
+                }
+            }
+            if (mIsVolumeController) {
+                synchronized (mExtVolumeControllerLock) {
+                    mExtVolumeController = null;
+                }
+            }
+            final long identity = Binder.clearCallingIdentity();
+            AudioSystem.registerPolicyMixes(mMixes, false);
+            Binder.restoreCallingIdentity(identity);
+            synchronized (mAudioPolicies) {
+                mAudioPolicies.remove(mPolicyCallback.asBinder());
+            }
+            try {
+                mPolicyCallback.notifyUnregistration();
+            } catch (RemoteException e) { }
+        }
+
+        boolean hasMixAffectingUsage(int usage, int excludedFlags) {
+            for (AudioMix mix : mMixes) {
+                if (mix.isAffectingUsage(usage)
+                        && ((mix.getRouteFlags() & excludedFlags) != excludedFlags)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // Verify all the devices in the array are served by mixes defined in this policy
+        boolean hasMixRoutedToDevices(@NonNull int[] deviceTypes,
+                @NonNull String[] deviceAddresses) {
+            for (int i = 0; i < deviceTypes.length; i++) {
+                boolean hasDevice = false;
+                for (AudioMix mix : mMixes) {
+                    // this will check both that the mix has ROUTE_FLAG_RENDER and the device
+                    // is reached by this mix
+                    if (mix.isRoutedToDevice(deviceTypes[i], deviceAddresses[i])) {
+                        hasDevice = true;
+                        break;
+                    }
+                }
+                if (!hasDevice) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        int addMixes(@NonNull ArrayList<AudioMix> mixes) {
+            // TODO optimize to not have to unregister the mixes already in place
+            synchronized (mMixes) {
+                AudioSystem.registerPolicyMixes(mMixes, false);
+                this.add(mixes);
+                return AudioSystem.registerPolicyMixes(mMixes, true);
+            }
+        }
+
+        int removeMixes(@NonNull ArrayList<AudioMix> mixes) {
+            // TODO optimize to not have to unregister the mixes already in place
+            synchronized (mMixes) {
+                AudioSystem.registerPolicyMixes(mMixes, false);
+                this.remove(mixes);
+                return AudioSystem.registerPolicyMixes(mMixes, true);
+            }
+        }
+
+        @AudioSystem.AudioSystemError int connectMixes() {
+            final long identity = Binder.clearCallingIdentity();
+            int status = AudioSystem.registerPolicyMixes(mMixes, true);
+            Binder.restoreCallingIdentity(identity);
+            return status;
+        }
+
+        int setUidDeviceAffinities(int uid, @NonNull int[] types, @NonNull String[] addresses) {
+            final Integer Uid = new Integer(uid);
+            if (mUidDeviceAffinities.remove(Uid) != null) {
+                if (removeUidDeviceAffinitiesFromSystem(uid) != AudioSystem.SUCCESS) {
+                    Log.e(TAG, "AudioSystem. removeUidDeviceAffinities(" + uid + ") failed, "
+                            + " cannot call AudioSystem.setUidDeviceAffinities");
+                    return AudioManager.ERROR;
+                }
+            }
+            AudioDeviceArray deviceArray = new AudioDeviceArray(types, addresses);
+            if (setUidDeviceAffinitiesOnSystem(uid, deviceArray) == AudioSystem.SUCCESS) {
+                mUidDeviceAffinities.put(Uid, deviceArray);
+                return AudioManager.SUCCESS;
+            }
+            Log.e(TAG, "AudioSystem. setUidDeviceAffinities(" + uid + ") failed");
+            return AudioManager.ERROR;
+        }
+
+        int removeUidDeviceAffinities(int uid) {
+            if (mUidDeviceAffinities.remove(new Integer(uid)) != null) {
+                if (removeUidDeviceAffinitiesFromSystem(uid) == AudioSystem.SUCCESS) {
+                    return AudioManager.SUCCESS;
+                }
+            }
+            Log.e(TAG, "AudioSystem. removeUidDeviceAffinities failed");
+            return AudioManager.ERROR;
+        }
+
+        @AudioSystem.AudioSystemError private int removeUidDeviceAffinitiesFromSystem(int uid) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return AudioSystem.removeUidDeviceAffinities(uid);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @AudioSystem.AudioSystemError private int setUidDeviceAffinitiesOnSystem(int uid,
+                AudioDeviceArray deviceArray) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return AudioSystem.setUidDeviceAffinities(uid, deviceArray.mDeviceTypes,
+                        deviceArray.mDeviceAddresses);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        int setUserIdDeviceAffinities(int userId,
+                @NonNull int[] types, @NonNull String[] addresses) {
+            final Integer UserId = new Integer(userId);
+            if (mUserIdDeviceAffinities.remove(UserId) != null) {
+                if (removeUserIdDeviceAffinitiesFromSystem(userId) != AudioSystem.SUCCESS) {
+                    Log.e(TAG, "AudioSystem. removeUserIdDeviceAffinities("
+                            + UserId + ") failed, "
+                            + " cannot call AudioSystem.setUserIdDeviceAffinities");
+                    return AudioManager.ERROR;
+                }
+            }
+            AudioDeviceArray audioDeviceArray = new AudioDeviceArray(types, addresses);
+            if (setUserIdDeviceAffinitiesOnSystem(userId, audioDeviceArray)
+                    == AudioSystem.SUCCESS) {
+                mUserIdDeviceAffinities.put(UserId, audioDeviceArray);
+                return AudioManager.SUCCESS;
+            }
+            Log.e(TAG, "AudioSystem.setUserIdDeviceAffinities(" + userId + ") failed");
+            return AudioManager.ERROR;
+        }
+
+        int removeUserIdDeviceAffinities(int userId) {
+            if (mUserIdDeviceAffinities.remove(new Integer(userId)) != null) {
+                if (removeUserIdDeviceAffinitiesFromSystem(userId) == AudioSystem.SUCCESS) {
+                    return AudioManager.SUCCESS;
+                }
+            }
+            Log.e(TAG, "AudioSystem.removeUserIdDeviceAffinities failed");
+            return AudioManager.ERROR;
+        }
+
+        @AudioSystem.AudioSystemError private int removeUserIdDeviceAffinitiesFromSystem(
+                @UserIdInt int userId) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return AudioSystem.removeUserIdDeviceAffinities(userId);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @AudioSystem.AudioSystemError private int setUserIdDeviceAffinitiesOnSystem(
+                @UserIdInt int userId, AudioDeviceArray deviceArray) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return AudioSystem.setUserIdDeviceAffinities(userId, deviceArray.mDeviceTypes,
+                        deviceArray.mDeviceAddresses);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @AudioSystem.AudioSystemError int setupDeviceAffinities() {
+            for (Map.Entry<Integer, AudioDeviceArray> uidEntry : mUidDeviceAffinities.entrySet()) {
+                int uidStatus = removeUidDeviceAffinitiesFromSystem(uidEntry.getKey());
+                if (uidStatus != AudioSystem.SUCCESS) {
+                    Log.e(TAG,
+                            "setupDeviceAffinities failed to remove device affinity for uid "
+                                    + uidEntry.getKey());
+                    return uidStatus;
+                }
+                uidStatus = setUidDeviceAffinitiesOnSystem(uidEntry.getKey(), uidEntry.getValue());
+                if (uidStatus != AudioSystem.SUCCESS) {
+                    Log.e(TAG,
+                            "setupDeviceAffinities failed to set device affinity for uid "
+                                    + uidEntry.getKey());
+                    return uidStatus;
+                }
+            }
+
+            for (Map.Entry<Integer, AudioDeviceArray> userIdEntry :
+                    mUserIdDeviceAffinities.entrySet()) {
+                int userIdStatus = removeUserIdDeviceAffinitiesFromSystem(userIdEntry.getKey());
+                if (userIdStatus != AudioSystem.SUCCESS) {
+                    Log.e(TAG,
+                            "setupDeviceAffinities failed to remove device affinity for userId "
+                                    + userIdEntry.getKey());
+                    return userIdStatus;
+                }
+                userIdStatus = setUserIdDeviceAffinitiesOnSystem(userIdEntry.getKey(),
+                                userIdEntry.getValue());
+                if (userIdStatus != AudioSystem.SUCCESS) {
+                    Log.e(TAG,
+                            "setupDeviceAffinities failed to set device affinity for userId "
+                                    + userIdEntry.getKey());
+                    return userIdStatus;
+                }
+            }
+            return AudioSystem.SUCCESS;
+        }
+
+        /** @return human readable debug informations summarizing the state of the object. */
+        public String toLogFriendlyString() {
+            String textDump = super.toLogFriendlyString();
+            textDump += " Uid Device Affinities:\n";
+            String spacer = "     ";
+            textDump += logFriendlyAttributeDeviceArrayMap("Uid",
+                    mUidDeviceAffinities, spacer);
+            textDump += " UserId Device Affinities:\n";
+            textDump += logFriendlyAttributeDeviceArrayMap("UserId",
+                    mUserIdDeviceAffinities, spacer);
+            textDump += " Proxy:\n";
+            textDump += "   is focus policy= " + mIsFocusPolicy + "\n";
+            if (mIsFocusPolicy) {
+                textDump += "     focus duck behaviour= " + mFocusDuckBehavior + "\n";
+                textDump += "     is test focus policy= " + mIsTestFocusPolicy + "\n";
+                textDump += "     has focus listener= " + mHasFocusListener  + "\n";
+            }
+            textDump += "   media projection= " + mProjection + "\n";
+            return textDump;
+        }
+
+        private String logFriendlyAttributeDeviceArrayMap(String attribute,
+                Map<Integer, AudioDeviceArray> map, String spacer) {
+            final StringBuilder stringBuilder = new StringBuilder();
+            for (Map.Entry<Integer, AudioDeviceArray> mapEntry : map.entrySet()) {
+                stringBuilder.append(spacer).append(attribute).append(": ")
+                        .append(mapEntry.getKey()).append("\n");
+                AudioDeviceArray deviceArray = mapEntry.getValue();
+                String deviceSpacer = spacer + "   ";
+                for (int i = 0; i < deviceArray.mDeviceTypes.length; i++) {
+                    stringBuilder.append(deviceSpacer).append("Type: 0x")
+                            .append(Integer.toHexString(deviceArray.mDeviceTypes[i]))
+                            .append(" Address: ").append(deviceArray.mDeviceAddresses[i])
+                                    .append("\n");
+                }
+            }
+            return stringBuilder.toString();
+        }
+    };
+
+    //======================
+    // Audio policy: focus
+    //======================
+    /**  */
+    public int dispatchFocusChange(AudioFocusInfo afi, int focusChange, IAudioPolicyCallback pcb) {
+        if (afi == null) {
+            throw new IllegalArgumentException("Illegal null AudioFocusInfo");
+        }
+        if (pcb == null) {
+            throw new IllegalArgumentException("Illegal null AudioPolicy callback");
+        }
+        synchronized (mAudioPolicies) {
+            if (!mAudioPolicies.containsKey(pcb.asBinder())) {
+                throw new IllegalStateException("Unregistered AudioPolicy for focus dispatch");
+            }
+            return mMediaFocusControl.dispatchFocusChange(afi, focusChange);
+        }
+    }
+
+    public void setFocusRequestResultFromExtPolicy(AudioFocusInfo afi, int requestResult,
+            IAudioPolicyCallback pcb) {
+        if (afi == null) {
+            throw new IllegalArgumentException("Illegal null AudioFocusInfo");
+        }
+        if (pcb == null) {
+            throw new IllegalArgumentException("Illegal null AudioPolicy callback");
+        }
+        synchronized (mAudioPolicies) {
+            if (!mAudioPolicies.containsKey(pcb.asBinder())) {
+                throw new IllegalStateException("Unregistered AudioPolicy for external focus");
+            }
+            mMediaFocusControl.setFocusRequestResultFromExtPolicy(afi, requestResult);
+        }
+    }
+
+
+    //======================
+    // Audioserver state displatch
+    //======================
+    private class AsdProxy implements IBinder.DeathRecipient {
+        private final IAudioServerStateDispatcher mAsd;
+
+        AsdProxy(IAudioServerStateDispatcher asd) {
+            mAsd = asd;
+        }
+
+        public void binderDied() {
+            synchronized (mAudioServerStateListeners) {
+                mAudioServerStateListeners.remove(mAsd.asBinder());
+            }
+        }
+
+        IAudioServerStateDispatcher callback() {
+            return mAsd;
+        }
+    }
+
+    private HashMap<IBinder, AsdProxy> mAudioServerStateListeners =
+            new HashMap<IBinder, AsdProxy>();
+
+    private void checkMonitorAudioServerStatePermission() {
+        if (!(mContext.checkCallingOrSelfPermission(
+                    android.Manifest.permission.MODIFY_PHONE_STATE) ==
+                PackageManager.PERMISSION_GRANTED ||
+              mContext.checkCallingOrSelfPermission(
+                    android.Manifest.permission.MODIFY_AUDIO_ROUTING) ==
+                PackageManager.PERMISSION_GRANTED)) {
+            throw new SecurityException("Not allowed to monitor audioserver state");
+        }
+    }
+
+    public void registerAudioServerStateDispatcher(IAudioServerStateDispatcher asd) {
+        checkMonitorAudioServerStatePermission();
+        synchronized (mAudioServerStateListeners) {
+            if (mAudioServerStateListeners.containsKey(asd.asBinder())) {
+                Slog.w(TAG, "Cannot re-register audio server state dispatcher");
+                return;
+            }
+            AsdProxy asdp = new AsdProxy(asd);
+            try {
+                asd.asBinder().linkToDeath(asdp, 0/*flags*/);
+            } catch (RemoteException e) {
+
+            }
+            mAudioServerStateListeners.put(asd.asBinder(), asdp);
+        }
+    }
+
+    public void unregisterAudioServerStateDispatcher(IAudioServerStateDispatcher asd) {
+        checkMonitorAudioServerStatePermission();
+        synchronized (mAudioServerStateListeners) {
+            AsdProxy asdp = mAudioServerStateListeners.remove(asd.asBinder());
+            if (asdp == null) {
+                Slog.w(TAG, "Trying to unregister unknown audioserver state dispatcher for pid "
+                        + Binder.getCallingPid() + " / uid " + Binder.getCallingUid());
+                return;
+            } else {
+                asd.asBinder().unlinkToDeath(asdp, 0/*flags*/);
+            }
+        }
+    }
+
+    public boolean isAudioServerRunning() {
+        checkMonitorAudioServerStatePermission();
+        return (AudioSystem.checkAudioFlinger() == AudioSystem.AUDIO_STATUS_OK);
+    }
+
+    //======================
+    // Audio HAL process dump
+    //======================
+
+    private static final String AUDIO_HAL_SERVICE_PREFIX = "android.hardware.audio";
+
+    private Set<Integer> getAudioHalPids() {
+        try {
+            IServiceManager serviceManager = IServiceManager.getService();
+            ArrayList<IServiceManager.InstanceDebugInfo> dump =
+                    serviceManager.debugDump();
+            HashSet<Integer> pids = new HashSet<>();
+            for (IServiceManager.InstanceDebugInfo info : dump) {
+                if (info.pid != IServiceManager.PidConstant.NO_PID
+                        && info.interfaceName != null
+                        && info.interfaceName.startsWith(AUDIO_HAL_SERVICE_PREFIX)) {
+                    pids.add(info.pid);
+                }
+            }
+            return pids;
+        } catch (RemoteException e) {
+            return new HashSet<Integer>();
+        }
+    }
+
+    private void updateAudioHalPids() {
+        Set<Integer> pidsSet = getAudioHalPids();
+        if (pidsSet.isEmpty()) {
+            Slog.w(TAG, "Could not retrieve audio HAL service pids");
+            return;
+        }
+        int[] pidsArray = pidsSet.stream().mapToInt(Integer::intValue).toArray();
+        AudioSystem.setAudioHalPids(pidsArray);
+    }
+
+    //======================
+    // Multi Audio Focus
+    //======================
+    public void setMultiAudioFocusEnabled(boolean enabled) {
+        enforceModifyAudioRoutingPermission();
+        if (mMediaFocusControl != null) {
+            boolean mafEnabled = mMediaFocusControl.getMultiAudioFocusEnabled();
+            if (mafEnabled != enabled) {
+                mMediaFocusControl.updateMultiAudioFocus(enabled);
+                if (!enabled) {
+                    mDeviceBroker.postBroadcastBecomingNoisy();
+                }
+            }
+        }
+    }
+
+
+    //======================
+    // misc
+    //======================
+    private final HashMap<IBinder, AudioPolicyProxy> mAudioPolicies =
+            new HashMap<IBinder, AudioPolicyProxy>();
+    @GuardedBy("mAudioPolicies")
+    private int mAudioPolicyCounter = 0;
+
+    //======================
+    // Helper functions for full and fixed volume device
+    //======================
+    private boolean isFixedVolumeDevice(int deviceType) {
+        if (deviceType == AudioSystem.DEVICE_OUT_REMOTE_SUBMIX
+                && mRecordMonitor.isLegacyRemoteSubmixActive()) {
+            return false;
+        }
+        return mFixedVolumeDevices.contains(deviceType);
+    }
+
+    private boolean isFullVolumeDevice(int deviceType) {
+        if (deviceType == AudioSystem.DEVICE_OUT_REMOTE_SUBMIX
+                && mRecordMonitor.isLegacyRemoteSubmixActive()) {
+            return false;
+        }
+        return mFullVolumeDevices.contains(deviceType);
+    }
+}
diff --git a/com/android/server/audio/AudioServiceEvents.java b/com/android/server/audio/AudioServiceEvents.java
new file mode 100644
index 0000000..5913567
--- /dev/null
+++ b/com/android/server/audio/AudioServiceEvents.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2017 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 android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.MediaMetrics;
+
+import com.android.server.audio.AudioDeviceInventory.WiredDeviceConnectionState;
+
+
+public class AudioServiceEvents {
+
+    final static class PhoneStateEvent extends AudioEventLogger.Event {
+        final String mPackage;
+        final int mOwnerPid;
+        final int mRequesterPid;
+        final int mRequestedMode;
+        final int mActualMode;
+
+        PhoneStateEvent(String callingPackage, int requesterPid, int requestedMode,
+                        int ownerPid, int actualMode) {
+            mPackage = callingPackage;
+            mRequesterPid = requesterPid;
+            mRequestedMode = requestedMode;
+            mOwnerPid = ownerPid;
+            mActualMode = actualMode;
+        }
+
+        @Override
+        public String eventToString() {
+            return new StringBuilder("setMode(").append(AudioSystem.modeToString(mRequestedMode))
+                    .append(") from package=").append(mPackage)
+                    .append(" pid=").append(mRequesterPid)
+                    .append(" selected mode=").append(AudioSystem.modeToString(mActualMode))
+                    .append(" by pid=").append(mOwnerPid).toString();
+        }
+    }
+
+    final static class WiredDevConnectEvent extends AudioEventLogger.Event {
+        final WiredDeviceConnectionState mState;
+
+        WiredDevConnectEvent(WiredDeviceConnectionState state) {
+            mState = state;
+        }
+
+        @Override
+        public String eventToString() {
+            return new StringBuilder("setWiredDeviceConnectionState(")
+                    .append(" type:").append(Integer.toHexString(mState.mType))
+                    .append(" state:").append(AudioSystem.deviceStateToString(mState.mState))
+                    .append(" addr:").append(mState.mAddress)
+                    .append(" name:").append(mState.mName)
+                    .append(") from ").append(mState.mCaller).toString();
+        }
+    }
+
+    final static class ForceUseEvent extends AudioEventLogger.Event {
+        final int mUsage;
+        final int mConfig;
+        final String mReason;
+
+        ForceUseEvent(int usage, int config, String reason) {
+            mUsage = usage;
+            mConfig = config;
+            mReason = reason;
+        }
+
+        @Override
+        public String eventToString() {
+            return new StringBuilder("setForceUse(")
+                    .append(AudioSystem.forceUseUsageToString(mUsage))
+                    .append(", ").append(AudioSystem.forceUseConfigToString(mConfig))
+                    .append(") due to ").append(mReason).toString();
+        }
+    }
+
+    final static class VolumeEvent extends AudioEventLogger.Event {
+        static final int VOL_ADJUST_SUGG_VOL = 0;
+        static final int VOL_ADJUST_STREAM_VOL = 1;
+        static final int VOL_SET_STREAM_VOL = 2;
+        static final int VOL_SET_HEARING_AID_VOL = 3;
+        static final int VOL_SET_AVRCP_VOL = 4;
+        static final int VOL_ADJUST_VOL_UID = 5;
+        static final int VOL_VOICE_ACTIVITY_HEARING_AID = 6;
+        static final int VOL_MODE_CHANGE_HEARING_AID = 7;
+        static final int VOL_SET_GROUP_VOL = 8;
+
+        final int mOp;
+        final int mStream;
+        final int mVal1;
+        final int mVal2;
+        final String mCaller;
+        final String mGroupName;
+        final AudioAttributes mAudioAttributes;
+
+        /** used for VOL_ADJUST_VOL_UID,
+         *           VOL_ADJUST_SUGG_VOL,
+         *           VOL_ADJUST_STREAM_VOL,
+         *           VOL_SET_STREAM_VOL */
+        VolumeEvent(int op, int stream, int val1, int val2, String caller) {
+            mOp = op;
+            mStream = stream;
+            mVal1 = val1;
+            mVal2 = val2;
+            mCaller = caller;
+            mGroupName = null;
+            mAudioAttributes = null;
+            logMetricEvent();
+        }
+
+        /** used for VOL_SET_HEARING_AID_VOL*/
+        VolumeEvent(int op, int index, int gainDb) {
+            mOp = op;
+            mVal1 = index;
+            mVal2 = gainDb;
+            // unused
+            mStream = -1;
+            mCaller = null;
+            mGroupName = null;
+            mAudioAttributes = null;
+            logMetricEvent();
+        }
+
+        /** used for VOL_SET_AVRCP_VOL */
+        VolumeEvent(int op, int index) {
+            mOp = op;
+            mVal1 = index;
+            // unused
+            mVal2 = 0;
+            mStream = -1;
+            mCaller = null;
+            mGroupName = null;
+            mAudioAttributes = null;
+            logMetricEvent();
+        }
+
+        /** used for VOL_VOICE_ACTIVITY_HEARING_AID */
+        VolumeEvent(int op, boolean voiceActive, int stream, int index) {
+            mOp = op;
+            mStream = stream;
+            mVal1 = index;
+            mVal2 = voiceActive ? 1 : 0;
+            // unused
+            mCaller = null;
+            mGroupName = null;
+            mAudioAttributes = null;
+            logMetricEvent();
+        }
+
+        /** used for VOL_MODE_CHANGE_HEARING_AID */
+        VolumeEvent(int op, int mode, int stream, int index) {
+            mOp = op;
+            mStream = stream;
+            mVal1 = index;
+            mVal2 = mode;
+            // unused
+            mCaller = null;
+            mGroupName = null;
+            mAudioAttributes = null;
+            logMetricEvent();
+        }
+
+        /** used for VOL_SET_GROUP_VOL */
+        VolumeEvent(int op, AudioAttributes aa, String group, int index, int flags, String caller) {
+            mOp = op;
+            mStream = -1;
+            mVal1 = index;
+            mVal2 = flags;
+            mCaller = caller;
+            mGroupName = group;
+            mAudioAttributes = aa;
+            logMetricEvent();
+        }
+
+
+        /**
+         * Audio Analytics unique Id.
+         */
+        private static final String mMetricsId = MediaMetrics.Name.AUDIO_VOLUME_EVENT;
+
+        /**
+         * Log mediametrics event
+         */
+        private void logMetricEvent() {
+            switch (mOp) {
+                case VOL_ADJUST_SUGG_VOL:
+                case VOL_ADJUST_VOL_UID:
+                case VOL_ADJUST_STREAM_VOL: {
+                    String eventName;
+                    switch (mOp) {
+                        case VOL_ADJUST_SUGG_VOL:
+                            eventName = "adjustSuggestedStreamVolume";
+                            break;
+                        case VOL_ADJUST_STREAM_VOL:
+                            eventName = "adjustStreamVolume";
+                            break;
+                        case VOL_ADJUST_VOL_UID:
+                            eventName = "adjustStreamVolumeForUid";
+                            break;
+                        default:
+                            return; // not possible, just return here
+                    }
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+                            .set(MediaMetrics.Property.DIRECTION, mVal1 > 0 ? "up" : "down")
+                            .set(MediaMetrics.Property.EVENT, eventName)
+                            .set(MediaMetrics.Property.FLAGS, mVal2)
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
+                            .record();
+                    return;
+                }
+                case VOL_SET_STREAM_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+                            .set(MediaMetrics.Property.EVENT, "setStreamVolume")
+                            .set(MediaMetrics.Property.FLAGS, mVal2)
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
+                            .record();
+                    return;
+                case VOL_SET_HEARING_AID_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.EVENT, "setHearingAidVolume")
+                            .set(MediaMetrics.Property.GAIN_DB, (double) mVal2)
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .record();
+                    return;
+                case VOL_SET_AVRCP_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.EVENT, "setAvrcpVolume")
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .record();
+                    return;
+                case VOL_VOICE_ACTIVITY_HEARING_AID:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.EVENT, "voiceActivityHearingAid")
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .set(MediaMetrics.Property.STATE,
+                                    mVal2 == 1 ? "active" : "inactive")
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
+                            .record();
+                    return;
+                case VOL_MODE_CHANGE_HEARING_AID:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.EVENT, "modeChangeHearingAid")
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .set(MediaMetrics.Property.MODE, AudioSystem.modeToString(mVal2))
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
+                            .record();
+                    return;
+                case VOL_SET_GROUP_VOL:
+                    new MediaMetrics.Item(mMetricsId)
+                            .set(MediaMetrics.Property.ATTRIBUTES, mAudioAttributes.toString())
+                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+                            .set(MediaMetrics.Property.EVENT, "setVolumeIndexForAttributes")
+                            .set(MediaMetrics.Property.FLAGS, mVal2)
+                            .set(MediaMetrics.Property.GROUP, mGroupName)
+                            .set(MediaMetrics.Property.INDEX, mVal1)
+                            .record();
+                    return;
+                default:
+                    return;
+            }
+        }
+
+        @Override
+        public String eventToString() {
+            switch (mOp) {
+                case VOL_ADJUST_SUGG_VOL:
+                    return new StringBuilder("adjustSuggestedStreamVolume(sugg:")
+                            .append(AudioSystem.streamToString(mStream))
+                            .append(" dir:").append(AudioManager.adjustToString(mVal1))
+                            .append(" flags:0x").append(Integer.toHexString(mVal2))
+                            .append(") from ").append(mCaller)
+                            .toString();
+                case VOL_ADJUST_STREAM_VOL:
+                    return new StringBuilder("adjustStreamVolume(stream:")
+                            .append(AudioSystem.streamToString(mStream))
+                            .append(" dir:").append(AudioManager.adjustToString(mVal1))
+                            .append(" flags:0x").append(Integer.toHexString(mVal2))
+                            .append(") from ").append(mCaller)
+                            .toString();
+                case VOL_SET_STREAM_VOL:
+                    return new StringBuilder("setStreamVolume(stream:")
+                            .append(AudioSystem.streamToString(mStream))
+                            .append(" index:").append(mVal1)
+                            .append(" flags:0x").append(Integer.toHexString(mVal2))
+                            .append(") from ").append(mCaller)
+                            .toString();
+                case VOL_SET_HEARING_AID_VOL:
+                    return new StringBuilder("setHearingAidVolume:")
+                            .append(" index:").append(mVal1)
+                            .append(" gain dB:").append(mVal2)
+                            .toString();
+                case VOL_SET_AVRCP_VOL:
+                    return new StringBuilder("setAvrcpVolume:")
+                            .append(" index:").append(mVal1)
+                            .toString();
+                case VOL_ADJUST_VOL_UID:
+                    return new StringBuilder("adjustStreamVolumeForUid(stream:")
+                            .append(AudioSystem.streamToString(mStream))
+                            .append(" dir:").append(AudioManager.adjustToString(mVal1))
+                            .append(" flags:0x").append(Integer.toHexString(mVal2))
+                            .append(") from ").append(mCaller)
+                            .toString();
+                case VOL_VOICE_ACTIVITY_HEARING_AID:
+                    return new StringBuilder("Voice activity change (")
+                            .append(mVal2 == 1 ? "active" : "inactive")
+                            .append(") causes setting HEARING_AID volume to idx:").append(mVal1)
+                            .append(" stream:").append(AudioSystem.streamToString(mStream))
+                            .toString();
+                case VOL_MODE_CHANGE_HEARING_AID:
+                    return new StringBuilder("setMode(")
+                            .append(AudioSystem.modeToString(mVal2))
+                            .append(") causes setting HEARING_AID volume to idx:").append(mVal1)
+                            .append(" stream:").append(AudioSystem.streamToString(mStream))
+                            .toString();
+                case VOL_SET_GROUP_VOL:
+                    return new StringBuilder("setVolumeIndexForAttributes(attr:")
+                            .append(mAudioAttributes.toString())
+                            .append(" group: ").append(mGroupName)
+                            .append(" index:").append(mVal1)
+                            .append(" flags:0x").append(Integer.toHexString(mVal2))
+                            .append(") from ").append(mCaller)
+                            .toString();
+                default: return new StringBuilder("FIXME invalid op:").append(mOp).toString();
+            }
+        }
+    }
+}
diff --git a/com/android/server/audio/AudioSystemAdapter.java b/com/android/server/audio/AudioSystemAdapter.java
new file mode 100644
index 0000000..e60243f
--- /dev/null
+++ b/com/android/server/audio/AudioSystemAdapter.java
@@ -0,0 +1,142 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioSystem;
+
+/**
+ * Provides an adapter to access functionality of the android.media.AudioSystem class for device
+ * related functionality.
+ * Use the "real" AudioSystem through the default adapter.
+ * Use the "always ok" adapter to avoid dealing with the APM behaviors during a test.
+ */
+public class AudioSystemAdapter {
+
+    /**
+     * Create a wrapper around the {@link AudioSystem} static methods, all functions are directly
+     * forwarded to the AudioSystem class.
+     * @return an adapter around AudioSystem
+     */
+    static final @NonNull AudioSystemAdapter getDefaultAdapter() {
+        return new AudioSystemAdapter();
+    }
+
+    /**
+     * Same as {@link AudioSystem#setDeviceConnectionState(int, int, String, String, int)}
+     * @param device
+     * @param state
+     * @param deviceAddress
+     * @param deviceName
+     * @param codecFormat
+     * @return
+     */
+    public int setDeviceConnectionState(int device, int state, String deviceAddress,
+                                        String deviceName, int codecFormat) {
+        return AudioSystem.setDeviceConnectionState(device, state, deviceAddress, deviceName,
+                codecFormat);
+    }
+
+    /**
+     * Same as {@link AudioSystem#getDeviceConnectionState(int, String)}
+     * @param device
+     * @param deviceAddress
+     * @return
+     */
+    public int getDeviceConnectionState(int device, String deviceAddress) {
+        return AudioSystem.getDeviceConnectionState(device, deviceAddress);
+    }
+
+    /**
+     * Same as {@link AudioSystem#handleDeviceConfigChange(int, String, String, int)}
+     * @param device
+     * @param deviceAddress
+     * @param deviceName
+     * @param codecFormat
+     * @return
+     */
+    public int handleDeviceConfigChange(int device, String deviceAddress,
+                                               String deviceName, int codecFormat) {
+        return AudioSystem.handleDeviceConfigChange(device, deviceAddress, deviceName,
+                codecFormat);
+    }
+
+    /**
+     * Same as {@link AudioSystem#setPreferredDeviceForStrategy(int, AudioDeviceAttributes)}
+     * @param strategy
+     * @param device
+     * @return
+     */
+    public int setPreferredDeviceForStrategy(int strategy, @NonNull AudioDeviceAttributes device) {
+        return AudioSystem.setPreferredDeviceForStrategy(strategy, device);
+    }
+
+    /**
+     * Same as {@link AudioSystem#removePreferredDeviceForStrategy(int)}
+     * @param strategy
+     * @return
+     */
+    public int removePreferredDeviceForStrategy(int strategy) {
+        return AudioSystem.removePreferredDeviceForStrategy(strategy);
+    }
+
+    /**
+     * Same as {@link AudioSystem#setParameters(String)}
+     * @param keyValuePairs
+     * @return
+     */
+    public int setParameters(String keyValuePairs) {
+        return AudioSystem.setParameters(keyValuePairs);
+    }
+
+    /**
+     * Same as {@link AudioSystem#isMicrophoneMuted()}}
+     * Checks whether the microphone mute is on or off.
+     * @return true if microphone is muted, false if it's not
+     */
+    public boolean isMicrophoneMuted() {
+        return AudioSystem.isMicrophoneMuted();
+    }
+
+    /**
+     * Same as {@link AudioSystem#muteMicrophone(boolean)}
+     * Sets the microphone mute on or off.
+     *
+     * @param on set <var>true</var> to mute the microphone;
+     *           <var>false</var> to turn mute off
+     * @return command completion status see AUDIO_STATUS_OK, see AUDIO_STATUS_ERROR
+     */
+    public int muteMicrophone(boolean on) {
+        return AudioSystem.muteMicrophone(on);
+    }
+
+    /**
+     * Same as {@link AudioSystem#setCurrentImeUid(int)}
+     * Communicate UID of current InputMethodService to audio policy service.
+     */
+    public int setCurrentImeUid(int uid) {
+        return AudioSystem.setCurrentImeUid(uid);
+    }
+
+    /**
+     * Same as {@link AudioSystem#isStreamActive(int, int)}
+     */
+    public boolean isStreamActive(int stream, int inPastMs) {
+        return AudioSystem.isStreamActive(stream, inPastMs);
+    }
+}
diff --git a/com/android/server/audio/BtHelper.java b/com/android/server/audio/BtHelper.java
new file mode 100644
index 0000000..0654f86
--- /dev/null
+++ b/com/android/server/audio/BtHelper.java
@@ -0,0 +1,1004 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothProfile;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+/**
+ * @hide
+ * Class to encapsulate all communication with Bluetooth services
+ */
+public class BtHelper {
+
+    private static final String TAG = "AS.BtHelper";
+
+    private final @NonNull AudioDeviceBroker mDeviceBroker;
+
+    BtHelper(@NonNull AudioDeviceBroker broker) {
+        mDeviceBroker = broker;
+    }
+
+    // List of clients having issued a SCO start request
+    @GuardedBy("BtHelper.this")
+    private final @NonNull ArrayList<ScoClient> mScoClients = new ArrayList<ScoClient>();
+
+    // BluetoothHeadset API to control SCO connection
+    private @Nullable BluetoothHeadset mBluetoothHeadset;
+
+    // Bluetooth headset device
+    private @Nullable BluetoothDevice mBluetoothHeadsetDevice;
+
+    private @Nullable BluetoothHearingAid mHearingAid;
+
+    // Reference to BluetoothA2dp to query for AbsoluteVolume.
+    private @Nullable BluetoothA2dp mA2dp;
+
+    // If absolute volume is supported in AVRCP device
+    private boolean mAvrcpAbsVolSupported = false;
+
+    // Current connection state indicated by bluetooth headset
+    private int mScoConnectionState;
+
+    // Indicate if SCO audio connection is currently active and if the initiator is
+    // audio service (internal) or bluetooth headset (external)
+    private int mScoAudioState;
+
+    // Indicates the mode used for SCO audio connection. The mode is virtual call if the request
+    // originated from an app targeting an API version before JB MR2 and raw audio after that.
+    private int mScoAudioMode;
+
+    // SCO audio state is not active
+    private static final int SCO_STATE_INACTIVE = 0;
+    // SCO audio activation request waiting for headset service to connect
+    private static final int SCO_STATE_ACTIVATE_REQ = 1;
+    // SCO audio state is active due to an action in BT handsfree (either voice recognition or
+    // in call audio)
+    private static final int SCO_STATE_ACTIVE_EXTERNAL = 2;
+    // SCO audio state is active or starting due to a request from AudioManager API
+    private static final int SCO_STATE_ACTIVE_INTERNAL = 3;
+    // SCO audio deactivation request waiting for headset service to connect
+    private static final int SCO_STATE_DEACTIVATE_REQ = 4;
+    // SCO audio deactivation in progress, waiting for Bluetooth audio intent
+    private static final int SCO_STATE_DEACTIVATING = 5;
+
+    // SCO audio mode is undefined
+    /*package*/  static final int SCO_MODE_UNDEFINED = -1;
+    // SCO audio mode is virtual voice call (BluetoothHeadset.startScoUsingVirtualVoiceCall())
+    /*package*/  static final int SCO_MODE_VIRTUAL_CALL = 0;
+    // SCO audio mode is raw audio (BluetoothHeadset.connectAudio())
+    private  static final int SCO_MODE_RAW = 1;
+    // SCO audio mode is Voice Recognition (BluetoothHeadset.startVoiceRecognition())
+    private  static final int SCO_MODE_VR = 2;
+    // max valid SCO audio mode values
+    private static final int SCO_MODE_MAX = 2;
+
+    private static final int BT_HEARING_AID_GAIN_MIN = -128;
+
+    /**
+     * Returns a string representation of the scoAudioMode.
+     */
+    public static String scoAudioModeToString(int scoAudioMode) {
+        switch (scoAudioMode) {
+            case SCO_MODE_UNDEFINED:
+                return "SCO_MODE_UNDEFINED";
+            case SCO_MODE_VIRTUAL_CALL:
+                return "SCO_MODE_VIRTUAL_CALL";
+            case SCO_MODE_RAW:
+                return "SCO_MODE_RAW";
+            case SCO_MODE_VR:
+                return "SCO_MODE_VR";
+            default:
+                return "SCO_MODE_(" + scoAudioMode + ")";
+        }
+    }
+
+    //----------------------------------------------------------------------
+    /*package*/ static class BluetoothA2dpDeviceInfo {
+        private final @NonNull BluetoothDevice mBtDevice;
+        private final int mVolume;
+        private final @AudioSystem.AudioFormatNativeEnumForBtCodec int mCodec;
+
+        BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice) {
+            this(btDevice, -1, AudioSystem.AUDIO_FORMAT_DEFAULT);
+        }
+
+        BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice, int volume, int codec) {
+            mBtDevice = btDevice;
+            mVolume = volume;
+            mCodec = codec;
+        }
+
+        public @NonNull BluetoothDevice getBtDevice() {
+            return mBtDevice;
+        }
+
+        public int getVolume() {
+            return mVolume;
+        }
+
+        public @AudioSystem.AudioFormatNativeEnumForBtCodec int getCodec() {
+            return mCodec;
+        }
+
+        // redefine equality op so we can match messages intended for this device
+        @Override
+        public boolean equals(Object o) {
+            if (o == null) {
+                return false;
+            }
+            if (this == o) {
+                return true;
+            }
+            if (o instanceof BluetoothA2dpDeviceInfo) {
+                return mBtDevice.equals(((BluetoothA2dpDeviceInfo) o).getBtDevice());
+            }
+            return false;
+        }
+
+
+    }
+
+    // A2DP device events
+    /*package*/ static final int EVENT_DEVICE_CONFIG_CHANGE = 0;
+    /*package*/ static final int EVENT_ACTIVE_DEVICE_CHANGE = 1;
+
+    /*package*/ static String a2dpDeviceEventToString(int event) {
+        switch (event) {
+            case EVENT_DEVICE_CONFIG_CHANGE: return "DEVICE_CONFIG_CHANGE";
+            case EVENT_ACTIVE_DEVICE_CHANGE: return "ACTIVE_DEVICE_CHANGE";
+            default:
+                return new String("invalid event:" + event);
+        }
+    }
+
+    /*package*/ @NonNull static String getName(@NonNull BluetoothDevice device) {
+        final String deviceName = device.getName();
+        if (deviceName == null) {
+            return "";
+        }
+        return deviceName;
+    }
+
+    //----------------------------------------------------------------------
+    // Interface for AudioDeviceBroker
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void onSystemReady() {
+        mScoConnectionState = android.media.AudioManager.SCO_AUDIO_STATE_ERROR;
+        resetBluetoothSco();
+        getBluetoothHeadset();
+
+        //FIXME: this is to maintain compatibility with deprecated intent
+        // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
+        Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
+        newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
+                AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+        sendStickyBroadcastToAll(newIntent);
+
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        if (adapter != null) {
+            adapter.getProfileProxy(mDeviceBroker.getContext(),
+                    mBluetoothProfileServiceListener, BluetoothProfile.A2DP);
+            adapter.getProfileProxy(mDeviceBroker.getContext(),
+                    mBluetoothProfileServiceListener, BluetoothProfile.HEARING_AID);
+        }
+    }
+
+    /*package*/ synchronized void onAudioServerDiedRestoreA2dp() {
+        final int forMed = mDeviceBroker.getBluetoothA2dpEnabled()
+                ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP;
+        mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, forMed, "onAudioServerDied()");
+    }
+
+    /*package*/ synchronized boolean isAvrcpAbsoluteVolumeSupported() {
+        return (mA2dp != null && mAvrcpAbsVolSupported);
+    }
+
+    /*package*/ synchronized void setAvrcpAbsoluteVolumeSupported(boolean supported) {
+        mAvrcpAbsVolSupported = supported;
+        Log.i(TAG, "setAvrcpAbsoluteVolumeSupported supported=" + supported);
+    }
+
+    /*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) {
+        if (mA2dp == null) {
+            if (AudioService.DEBUG_VOL) {
+                AudioService.sVolumeLogger.log(new AudioEventLogger.StringEvent(
+                        "setAvrcpAbsoluteVolumeIndex: bailing due to null mA2dp").printLog(TAG));
+                return;
+            }
+        }
+        if (!mAvrcpAbsVolSupported) {
+            AudioService.sVolumeLogger.log(new AudioEventLogger.StringEvent(
+                    "setAvrcpAbsoluteVolumeIndex: abs vol not supported ").printLog(TAG));
+            return;
+        }
+        if (AudioService.DEBUG_VOL) {
+            Log.i(TAG, "setAvrcpAbsoluteVolumeIndex index=" + index);
+        }
+        AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
+                AudioServiceEvents.VolumeEvent.VOL_SET_AVRCP_VOL, index));
+        mA2dp.setAvrcpAbsoluteVolume(index);
+    }
+
+    /*package*/ synchronized @AudioSystem.AudioFormatNativeEnumForBtCodec int getA2dpCodec(
+            @NonNull BluetoothDevice device) {
+        if (mA2dp == null) {
+            return AudioSystem.AUDIO_FORMAT_DEFAULT;
+        }
+        final BluetoothCodecStatus btCodecStatus = mA2dp.getCodecStatus(device);
+        if (btCodecStatus == null) {
+            return AudioSystem.AUDIO_FORMAT_DEFAULT;
+        }
+        final BluetoothCodecConfig btCodecConfig = btCodecStatus.getCodecConfig();
+        if (btCodecConfig == null) {
+            return AudioSystem.AUDIO_FORMAT_DEFAULT;
+        }
+        return AudioSystem.bluetoothCodecToAudioFormat(btCodecConfig.getCodecType());
+    }
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void receiveBtEvent(Intent intent) {
+        final String action = intent.getAction();
+        if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
+            BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+            setBtScoActiveDevice(btDevice);
+        } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+            boolean broadcast = false;
+            int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
+            int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+            // broadcast intent if the connection was initated by AudioService
+            if (!mScoClients.isEmpty()
+                    && (mScoAudioState == SCO_STATE_ACTIVE_INTERNAL
+                    || mScoAudioState == SCO_STATE_ACTIVATE_REQ
+                    || mScoAudioState == SCO_STATE_DEACTIVATE_REQ
+                    || mScoAudioState == SCO_STATE_DEACTIVATING)) {
+                broadcast = true;
+            }
+            switch (btState) {
+                case BluetoothHeadset.STATE_AUDIO_CONNECTED:
+                    scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
+                    if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
+                            && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
+                        mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
+                    }
+                    mDeviceBroker.setBluetoothScoOn(true, "BtHelper.receiveBtEvent");
+                    break;
+                case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
+                    mDeviceBroker.setBluetoothScoOn(false, "BtHelper.receiveBtEvent");
+                    scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
+                    // startBluetoothSco called after stopBluetoothSco
+                    if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) {
+                        if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null
+                                && connectBluetoothScoAudioHelper(mBluetoothHeadset,
+                                mBluetoothHeadsetDevice, mScoAudioMode)) {
+                            mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                            broadcast = false;
+                            break;
+                        }
+                    }
+                    // Tear down SCO if disconnected from external
+                    clearAllScoClients(0, mScoAudioState == SCO_STATE_ACTIVE_INTERNAL);
+                    mScoAudioState = SCO_STATE_INACTIVE;
+                    break;
+                case BluetoothHeadset.STATE_AUDIO_CONNECTING:
+                    if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
+                            && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
+                        mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
+                    }
+                    broadcast = false;
+                    break;
+                default:
+                    // do not broadcast CONNECTING or invalid state
+                    broadcast = false;
+                    break;
+            }
+            if (broadcast) {
+                broadcastScoConnectionState(scoAudioState);
+                //FIXME: this is to maintain compatibility with deprecated intent
+                // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
+                Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
+                newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, scoAudioState);
+                sendStickyBroadcastToAll(newIntent);
+            }
+        }
+    }
+
+    /**
+     *
+     * @return false if SCO isn't connected
+     */
+    /*package*/ synchronized boolean isBluetoothScoOn() {
+        if ((mBluetoothHeadset != null)
+                && (mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
+                != BluetoothHeadset.STATE_AUDIO_CONNECTED)) {
+            Log.w(TAG, "isBluetoothScoOn(true) returning false because "
+                    + mBluetoothHeadsetDevice + " is not in audio connected mode");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Disconnect all SCO connections started by {@link AudioManager} except those started by
+     * {@param exceptPid}
+     *
+     * @param exceptPid pid whose SCO connections through {@link AudioManager} should be kept
+     */
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void disconnectBluetoothSco(int exceptPid) {
+        checkScoAudioState();
+        if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL) {
+            return;
+        }
+        clearAllScoClients(exceptPid, true);
+    }
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void startBluetoothScoForClient(IBinder cb, int scoAudioMode,
+                @NonNull String eventSource) {
+        ScoClient client = getScoClient(cb, true);
+        // The calling identity must be cleared before calling ScoClient.incCount().
+        // inCount() calls requestScoState() which in turn can call BluetoothHeadset APIs
+        // and this must be done on behalf of system server to make sure permissions are granted.
+        // The caller identity must be cleared after getScoClient() because it is needed if a new
+        // client is created.
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource));
+            client.requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode);
+        } catch (NullPointerException e) {
+            Log.e(TAG, "Null ScoClient", e);
+        }
+        Binder.restoreCallingIdentity(ident);
+    }
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void stopBluetoothScoForClient(IBinder cb,
+            @NonNull String eventSource) {
+        ScoClient client = getScoClient(cb, false);
+        // The calling identity must be cleared before calling ScoClient.decCount().
+        // decCount() calls requestScoState() which in turn can call BluetoothHeadset APIs
+        // and this must be done on behalf of system server to make sure permissions are granted.
+        final long ident = Binder.clearCallingIdentity();
+        if (client != null) {
+            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource));
+            client.requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED,
+                    SCO_MODE_VIRTUAL_CALL);
+            // If a disconnection is pending, the client will be removed whne clearAllScoClients()
+            // is called form receiveBtEvent()
+            if (mScoAudioState != SCO_STATE_DEACTIVATE_REQ
+                    && mScoAudioState != SCO_STATE_DEACTIVATING) {
+                client.remove(false /*stop */, true /*unregister*/);
+            }
+        }
+        Binder.restoreCallingIdentity(ident);
+    }
+
+
+    /*package*/ synchronized void setHearingAidVolume(int index, int streamType) {
+        if (mHearingAid == null) {
+            if (AudioService.DEBUG_VOL) {
+                Log.i(TAG, "setHearingAidVolume: null mHearingAid");
+            }
+            return;
+        }
+        //hearing aid expect volume value in range -128dB to 0dB
+        int gainDB = (int) AudioSystem.getStreamVolumeDB(streamType, index / 10,
+                AudioSystem.DEVICE_OUT_HEARING_AID);
+        if (gainDB < BT_HEARING_AID_GAIN_MIN) {
+            gainDB = BT_HEARING_AID_GAIN_MIN;
+        }
+        if (AudioService.DEBUG_VOL) {
+            Log.i(TAG, "setHearingAidVolume: calling mHearingAid.setVolume idx="
+                    + index + " gain=" + gainDB);
+        }
+        AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
+                AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB));
+        mHearingAid.setVolume(gainDB);
+    }
+
+    /*package*/ synchronized void onBroadcastScoConnectionState(int state) {
+        if (state == mScoConnectionState) {
+            return;
+        }
+        Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
+        newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, state);
+        newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE,
+                mScoConnectionState);
+        sendStickyBroadcastToAll(newIntent);
+        mScoConnectionState = state;
+    }
+
+    /*package*/ synchronized void disconnectAllBluetoothProfiles() {
+        mDeviceBroker.postDisconnectA2dp();
+        mDeviceBroker.postDisconnectA2dpSink();
+        mDeviceBroker.postDisconnectHeadset();
+        mDeviceBroker.postDisconnectHearingAid();
+    }
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void resetBluetoothSco() {
+        clearAllScoClients(0, false);
+        mScoAudioState = SCO_STATE_INACTIVE;
+        broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+        AudioSystem.setParameters("A2dpSuspended=false");
+        mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco");
+    }
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void disconnectHeadset() {
+        setBtScoActiveDevice(null);
+        mBluetoothHeadset = null;
+    }
+
+    /*package*/ synchronized void onA2dpProfileConnected(BluetoothA2dp a2dp) {
+        mA2dp = a2dp;
+        final List<BluetoothDevice> deviceList = mA2dp.getConnectedDevices();
+        if (deviceList.isEmpty()) {
+            return;
+        }
+        final BluetoothDevice btDevice = deviceList.get(0);
+        // the device is guaranteed CONNECTED
+        mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(btDevice,
+                BluetoothA2dp.STATE_CONNECTED, BluetoothProfile.A2DP_SINK, true, -1);
+    }
+
+    /*package*/ synchronized void onA2dpSinkProfileConnected(BluetoothProfile profile) {
+        final List<BluetoothDevice> deviceList = profile.getConnectedDevices();
+        if (deviceList.isEmpty()) {
+            return;
+        }
+        final BluetoothDevice btDevice = deviceList.get(0);
+        final @BluetoothProfile.BtProfileState int state =
+                profile.getConnectionState(btDevice);
+        mDeviceBroker.postSetA2dpSourceConnectionState(
+                state, new BluetoothA2dpDeviceInfo(btDevice));
+    }
+
+    /*package*/ synchronized void onHearingAidProfileConnected(BluetoothHearingAid hearingAid) {
+        mHearingAid = hearingAid;
+        final List<BluetoothDevice> deviceList = mHearingAid.getConnectedDevices();
+        if (deviceList.isEmpty()) {
+            return;
+        }
+        final BluetoothDevice btDevice = deviceList.get(0);
+        final @BluetoothProfile.BtProfileState int state =
+                mHearingAid.getConnectionState(btDevice);
+        mDeviceBroker.postBluetoothHearingAidDeviceConnectionState(
+                btDevice, state,
+                /*suppressNoisyIntent*/ false,
+                /*musicDevice*/ android.media.AudioSystem.DEVICE_NONE,
+                /*eventSource*/ "mBluetoothProfileServiceListener");
+    }
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void onHeadsetProfileConnected(BluetoothHeadset headset) {
+        // Discard timeout message
+        mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService();
+        mBluetoothHeadset = headset;
+        setBtScoActiveDevice(mBluetoothHeadset.getActiveDevice());
+        // Refresh SCO audio state
+        checkScoAudioState();
+        if (mScoAudioState != SCO_STATE_ACTIVATE_REQ
+                && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
+            return;
+        }
+        boolean status = false;
+        if (mBluetoothHeadsetDevice != null) {
+            switch (mScoAudioState) {
+                case SCO_STATE_ACTIVATE_REQ:
+                    status = connectBluetoothScoAudioHelper(
+                            mBluetoothHeadset,
+                            mBluetoothHeadsetDevice, mScoAudioMode);
+                    if (status) {
+                        mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                    }
+                    break;
+                case SCO_STATE_DEACTIVATE_REQ:
+                    status = disconnectBluetoothScoAudioHelper(
+                            mBluetoothHeadset,
+                            mBluetoothHeadsetDevice, mScoAudioMode);
+                    if (status) {
+                        mScoAudioState = SCO_STATE_DEACTIVATING;
+                    }
+                    break;
+            }
+        }
+        if (!status) {
+            mScoAudioState = SCO_STATE_INACTIVE;
+            broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+        }
+    }
+
+    //----------------------------------------------------------------------
+    private void broadcastScoConnectionState(int state) {
+        mDeviceBroker.postBroadcastScoConnectionState(state);
+    }
+
+    private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) {
+        if (btDevice == null) {
+            return true;
+        }
+        String address = btDevice.getAddress();
+        BluetoothClass btClass = btDevice.getBluetoothClass();
+        int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET;
+        int[] outDeviceTypes = {
+                AudioSystem.DEVICE_OUT_BLUETOOTH_SCO,
+                AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET,
+                AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT
+        };
+        if (btClass != null) {
+            switch (btClass.getDeviceClass()) {
+                case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
+                case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
+                    outDeviceTypes = new int[] { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET };
+                    break;
+                case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
+                    outDeviceTypes = new int[] { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT };
+                    break;
+            }
+        }
+        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+            address = "";
+        }
+        String btDeviceName =  getName(btDevice);
+        boolean result = false;
+        if (isActive) {
+            result |= mDeviceBroker.handleDeviceConnection(
+                    isActive, outDeviceTypes[0], address, btDeviceName);
+        } else {
+            for (int outDeviceType : outDeviceTypes) {
+                result |= mDeviceBroker.handleDeviceConnection(
+                        isActive, outDeviceType, address, btDeviceName);
+            }
+        }
+        // handleDeviceConnection() && result to make sure the method get executed
+        result = mDeviceBroker.handleDeviceConnection(
+                isActive, inDevice, address, btDeviceName) && result;
+        return result;
+    }
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    @GuardedBy("BtHelper.this")
+    private void setBtScoActiveDevice(BluetoothDevice btDevice) {
+        Log.i(TAG, "setBtScoActiveDevice: " + mBluetoothHeadsetDevice + " -> " + btDevice);
+        final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice;
+        if (Objects.equals(btDevice, previousActiveDevice)) {
+            return;
+        }
+        if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) {
+            Log.w(TAG, "setBtScoActiveDevice() failed to remove previous device "
+                    + previousActiveDevice);
+        }
+        if (!handleBtScoActiveDeviceChange(btDevice, true)) {
+            Log.e(TAG, "setBtScoActiveDevice() failed to add new device " + btDevice);
+            // set mBluetoothHeadsetDevice to null when failing to add new device
+            btDevice = null;
+        }
+        mBluetoothHeadsetDevice = btDevice;
+        if (mBluetoothHeadsetDevice == null) {
+            resetBluetoothSco();
+        }
+    }
+
+    // NOTE this listener is NOT called from AudioDeviceBroker event thread, only call async
+    //      methods inside listener.
+    private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
+            new BluetoothProfile.ServiceListener() {
+                public void onServiceConnected(int profile, BluetoothProfile proxy) {
+                    switch(profile) {
+                        case BluetoothProfile.A2DP:
+                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                                    "BT profile service: connecting A2DP profile"));
+                            mDeviceBroker.postBtA2dpProfileConnected((BluetoothA2dp) proxy);
+                            break;
+
+                        case BluetoothProfile.A2DP_SINK:
+                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                                    "BT profile service: connecting A2DP_SINK profile"));
+                            mDeviceBroker.postBtA2dpSinkProfileConnected(proxy);
+                            break;
+
+                        case BluetoothProfile.HEADSET:
+                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                                    "BT profile service: connecting HEADSET profile"));
+                            mDeviceBroker.postBtHeasetProfileConnected((BluetoothHeadset) proxy);
+                            break;
+
+                        case BluetoothProfile.HEARING_AID:
+                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+                                    "BT profile service: connecting HEARING_AID profile"));
+                            mDeviceBroker.postBtHearingAidProfileConnected(
+                                    (BluetoothHearingAid) proxy);
+                            break;
+                        default:
+                            break;
+                    }
+                }
+                public void onServiceDisconnected(int profile) {
+
+                    switch (profile) {
+                        case BluetoothProfile.A2DP:
+                            mDeviceBroker.postDisconnectA2dp();
+                            break;
+
+                        case BluetoothProfile.A2DP_SINK:
+                            mDeviceBroker.postDisconnectA2dpSink();
+                            break;
+
+                        case BluetoothProfile.HEADSET:
+                            mDeviceBroker.postDisconnectHeadset();
+                            break;
+
+                        case BluetoothProfile.HEARING_AID:
+                            mDeviceBroker.postDisconnectHearingAid();
+                            break;
+
+                        default:
+                            break;
+                    }
+                }
+            };
+
+    //----------------------------------------------------------------------
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    /*package*/ synchronized void scoClientDied(Object obj) {
+        final ScoClient client = (ScoClient) obj;
+        client.remove(true /*stop*/, false /*unregister*/);
+        Log.w(TAG, "SCO client died");
+    }
+
+    private class ScoClient implements IBinder.DeathRecipient {
+        private IBinder mCb; // To be notified of client's death
+        private int mCreatorPid;
+
+        ScoClient(IBinder cb) {
+            mCb = cb;
+            mCreatorPid = Binder.getCallingPid();
+        }
+
+        public void registerDeathRecipient() {
+            try {
+                mCb.linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                Log.w(TAG, "ScoClient could not link to " + mCb + " binder death");
+            }
+        }
+
+        public void unregisterDeathRecipient() {
+            try {
+                mCb.unlinkToDeath(this, 0);
+            } catch (NoSuchElementException e) {
+                Log.w(TAG, "ScoClient could not not unregistered to binder");
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            // process this from DeviceBroker's message queue to take the right locks since
+            // this event can impact SCO mode and requires querying audio mode stack
+            mDeviceBroker.postScoClientDied(this);
+        }
+
+        IBinder getBinder() {
+            return mCb;
+        }
+
+        int getPid() {
+            return mCreatorPid;
+        }
+
+        // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+        //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+        @GuardedBy("BtHelper.this")
+        private boolean requestScoState(int state, int scoAudioMode) {
+            checkScoAudioState();
+            if (mScoClients.size() != 1) {
+                Log.i(TAG, "requestScoState: state=" + state + ", scoAudioMode=" + scoAudioMode
+                        + ", num SCO clients=" + mScoClients.size());
+                return true;
+            }
+            if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
+                // Make sure that the state transitions to CONNECTING even if we cannot initiate
+                // the connection.
+                broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING);
+                // Accept SCO audio activation only in NORMAL audio mode or if the mode is
+                // currently controlled by the same client process.
+                final int modeOwnerPid =  mDeviceBroker.getModeOwnerPid();
+                if (modeOwnerPid != 0 && (modeOwnerPid != mCreatorPid)) {
+                    Log.w(TAG, "requestScoState: audio mode is not NORMAL and modeOwnerPid "
+                            + modeOwnerPid + " != creatorPid " + mCreatorPid);
+                    broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                    return false;
+                }
+                switch (mScoAudioState) {
+                    case SCO_STATE_INACTIVE:
+                        mScoAudioMode = scoAudioMode;
+                        if (scoAudioMode == SCO_MODE_UNDEFINED) {
+                            mScoAudioMode = SCO_MODE_VIRTUAL_CALL;
+                            if (mBluetoothHeadsetDevice != null) {
+                                mScoAudioMode = Settings.Global.getInt(
+                                        mDeviceBroker.getContentResolver(),
+                                        "bluetooth_sco_channel_"
+                                                + mBluetoothHeadsetDevice.getAddress(),
+                                        SCO_MODE_VIRTUAL_CALL);
+                                if (mScoAudioMode > SCO_MODE_MAX || mScoAudioMode < 0) {
+                                    mScoAudioMode = SCO_MODE_VIRTUAL_CALL;
+                                }
+                            }
+                        }
+                        if (mBluetoothHeadset == null) {
+                            if (getBluetoothHeadset()) {
+                                mScoAudioState = SCO_STATE_ACTIVATE_REQ;
+                            } else {
+                                Log.w(TAG, "requestScoState: getBluetoothHeadset failed during"
+                                        + " connection, mScoAudioMode=" + mScoAudioMode);
+                                broadcastScoConnectionState(
+                                        AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                                return false;
+                            }
+                            break;
+                        }
+                        if (mBluetoothHeadsetDevice == null) {
+                            Log.w(TAG, "requestScoState: no active device while connecting,"
+                                    + " mScoAudioMode=" + mScoAudioMode);
+                            broadcastScoConnectionState(
+                                    AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                            return false;
+                        }
+                        if (connectBluetoothScoAudioHelper(mBluetoothHeadset,
+                                mBluetoothHeadsetDevice, mScoAudioMode)) {
+                            mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                        } else {
+                            Log.w(TAG, "requestScoState: connect to " + mBluetoothHeadsetDevice
+                                    + " failed, mScoAudioMode=" + mScoAudioMode);
+                            broadcastScoConnectionState(
+                                    AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                            return false;
+                        }
+                        break;
+                    case SCO_STATE_DEACTIVATING:
+                        mScoAudioState = SCO_STATE_ACTIVATE_REQ;
+                        break;
+                    case SCO_STATE_DEACTIVATE_REQ:
+                        mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                        broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED);
+                        break;
+                    case SCO_STATE_ACTIVE_INTERNAL:
+                        Log.w(TAG, "requestScoState: already in ACTIVE mode, simply return");
+                        break;
+                    default:
+                        Log.w(TAG, "requestScoState: failed to connect in state "
+                                + mScoAudioState + ", scoAudioMode=" + scoAudioMode);
+                        broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                        return false;
+                }
+            } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
+                switch (mScoAudioState) {
+                    case SCO_STATE_ACTIVE_INTERNAL:
+                        if (mBluetoothHeadset == null) {
+                            if (getBluetoothHeadset()) {
+                                mScoAudioState = SCO_STATE_DEACTIVATE_REQ;
+                            } else {
+                                Log.w(TAG, "requestScoState: getBluetoothHeadset failed during"
+                                        + " disconnection, mScoAudioMode=" + mScoAudioMode);
+                                mScoAudioState = SCO_STATE_INACTIVE;
+                                broadcastScoConnectionState(
+                                        AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                                return false;
+                            }
+                            break;
+                        }
+                        if (mBluetoothHeadsetDevice == null) {
+                            mScoAudioState = SCO_STATE_INACTIVE;
+                            broadcastScoConnectionState(
+                                    AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                            break;
+                        }
+                        if (disconnectBluetoothScoAudioHelper(mBluetoothHeadset,
+                                mBluetoothHeadsetDevice, mScoAudioMode)) {
+                            mScoAudioState = SCO_STATE_DEACTIVATING;
+                        } else {
+                            mScoAudioState = SCO_STATE_INACTIVE;
+                            broadcastScoConnectionState(
+                                    AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                        }
+                        break;
+                    case SCO_STATE_ACTIVATE_REQ:
+                        mScoAudioState = SCO_STATE_INACTIVE;
+                        broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                        break;
+                    default:
+                        Log.w(TAG, "requestScoState: failed to disconnect in state "
+                                + mScoAudioState + ", scoAudioMode=" + scoAudioMode);
+                        broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                        return false;
+                }
+            }
+            return true;
+        }
+
+        @GuardedBy("BtHelper.this")
+        void remove(boolean stop, boolean unregister) {
+            if (unregister) {
+                unregisterDeathRecipient();
+            }
+            if (stop) {
+                requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED,
+                        SCO_MODE_VIRTUAL_CALL);
+            }
+            mScoClients.remove(this);
+        }
+    }
+
+    //-----------------------------------------------------
+    // Utilities
+    private void sendStickyBroadcastToAll(Intent intent) {
+        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mDeviceBroker.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private static boolean disconnectBluetoothScoAudioHelper(BluetoothHeadset bluetoothHeadset,
+            BluetoothDevice device, int scoAudioMode) {
+        switch (scoAudioMode) {
+            case SCO_MODE_RAW:
+                return bluetoothHeadset.disconnectAudio();
+            case SCO_MODE_VIRTUAL_CALL:
+                return bluetoothHeadset.stopScoUsingVirtualVoiceCall();
+            case SCO_MODE_VR:
+                return bluetoothHeadset.stopVoiceRecognition(device);
+            default:
+                return false;
+        }
+    }
+
+    private static boolean connectBluetoothScoAudioHelper(BluetoothHeadset bluetoothHeadset,
+            BluetoothDevice device, int scoAudioMode) {
+        switch (scoAudioMode) {
+            case SCO_MODE_RAW:
+                return bluetoothHeadset.connectAudio();
+            case SCO_MODE_VIRTUAL_CALL:
+                return bluetoothHeadset.startScoUsingVirtualVoiceCall();
+            case SCO_MODE_VR:
+                return bluetoothHeadset.startVoiceRecognition(device);
+            default:
+                return false;
+        }
+    }
+
+    private void checkScoAudioState() {
+        if (mBluetoothHeadset != null
+                && mBluetoothHeadsetDevice != null
+                && mScoAudioState == SCO_STATE_INACTIVE
+                && mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
+                != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
+            mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
+        }
+    }
+
+
+    @GuardedBy("BtHelper.this")
+    private ScoClient getScoClient(IBinder cb, boolean create) {
+        for (ScoClient existingClient : mScoClients) {
+            if (existingClient.getBinder() == cb) {
+                return existingClient;
+            }
+        }
+        if (create) {
+            ScoClient newClient = new ScoClient(cb);
+            newClient.registerDeathRecipient();
+            mScoClients.add(newClient);
+            return newClient;
+        }
+        return null;
+    }
+
+    // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+    //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
+    @GuardedBy("BtHelper.this")
+    private void clearAllScoClients(int exceptPid, boolean stopSco) {
+        final ArrayList<ScoClient> clients = new ArrayList<ScoClient>();
+        for (ScoClient cl : mScoClients) {
+            if (cl.getPid() != exceptPid) {
+                clients.add(cl);
+            }
+        }
+        for (ScoClient cl : clients) {
+            cl.remove(stopSco, true /*unregister*/);
+        }
+
+    }
+
+    private boolean getBluetoothHeadset() {
+        boolean result = false;
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        if (adapter != null) {
+            result = adapter.getProfileProxy(mDeviceBroker.getContext(),
+                    mBluetoothProfileServiceListener, BluetoothProfile.HEADSET);
+        }
+        // If we could not get a bluetooth headset proxy, send a failure message
+        // without delay to reset the SCO audio state and clear SCO clients.
+        // If we could get a proxy, send a delayed failure message that will reset our state
+        // in case we don't receive onServiceConnected().
+        mDeviceBroker.handleFailureToConnectToBtHeadsetService(
+                result ? AudioDeviceBroker.BT_HEADSET_CNCT_TIMEOUT_MS : 0);
+        return result;
+    }
+
+    /**
+     * Returns the String equivalent of the btCodecType.
+     *
+     * This uses an "ENCODING_" prefix for consistency with Audio;
+     * we could alternately use the "SOURCE_CODEC_TYPE_" prefix from Bluetooth.
+     */
+    public static String bluetoothCodecToEncodingString(int btCodecType) {
+        switch (btCodecType) {
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
+                return "ENCODING_SBC";
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
+                return "ENCODING_AAC";
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
+                return "ENCODING_APTX";
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
+                return "ENCODING_APTX_HD";
+            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
+                return "ENCODING_LDAC";
+            default:
+                return "ENCODING_BT_CODEC_TYPE(" + btCodecType + ")";
+        }
+    }
+}
diff --git a/com/android/server/audio/FocusRequester.java b/com/android/server/audio/FocusRequester.java
new file mode 100644
index 0000000..41008c2
--- /dev/null
+++ b/com/android/server/audio/FocusRequester.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright (C) 2013 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.AudioAttributes;
+import android.media.AudioFocusInfo;
+import android.media.AudioManager;
+import android.media.IAudioFocusDispatcher;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.audio.MediaFocusControl.AudioFocusDeathHandler;
+
+import java.io.PrintWriter;
+
+/**
+ * @hide
+ * Class to handle all the information about a user of audio focus. The lifecycle of each
+ * instance is managed by android.media.MediaFocusControl, from its addition to the audio focus
+ * stack, or the map of focus owners for an external focus policy, to its release.
+ */
+public class FocusRequester {
+
+    // on purpose not using this classe's name, as it will only be used from MediaFocusControl
+    private static final String TAG = "MediaFocusControl";
+    private static final boolean DEBUG = false;
+
+    private AudioFocusDeathHandler mDeathHandler; // may be null
+    private IAudioFocusDispatcher mFocusDispatcher; // may be null
+    private final IBinder mSourceRef; // may be null
+    private final @NonNull String mClientId;
+    private final @NonNull String mPackageName;
+    private final int mCallingUid;
+    private final MediaFocusControl mFocusController; // never null
+    private final int mSdkTarget;
+
+    /**
+     * the audio focus gain request that caused the addition of this object in the focus stack.
+     */
+    private final int mFocusGainRequest;
+    /**
+     * the flags associated with the gain request that qualify the type of grant (e.g. accepting
+     * delay vs grant must be immediate)
+     */
+    private final int mGrantFlags;
+    /**
+     * the audio focus loss received my mFocusDispatcher, is AudioManager.AUDIOFOCUS_NONE if
+     *  it never lost focus.
+     */
+    private int mFocusLossReceived;
+    /**
+     * whether this focus owner listener was notified when it lost focus
+     */
+    private boolean mFocusLossWasNotified;
+    /**
+     * the audio attributes associated with the focus request
+     */
+    private final @NonNull AudioAttributes mAttributes;
+
+    /**
+     * Class constructor
+     * @param aa
+     * @param focusRequest
+     * @param grantFlags
+     * @param afl
+     * @param source
+     * @param id
+     * @param hdlr
+     * @param pn
+     * @param uid
+     * @param ctlr cannot be null
+     */
+    FocusRequester(@NonNull AudioAttributes aa, int focusRequest, int grantFlags,
+            IAudioFocusDispatcher afl, IBinder source, @NonNull String id,
+            AudioFocusDeathHandler hdlr, @NonNull String pn, int uid,
+            @NonNull MediaFocusControl ctlr, int sdk) {
+        mAttributes = aa;
+        mFocusDispatcher = afl;
+        mSourceRef = source;
+        mClientId = id;
+        mDeathHandler = hdlr;
+        mPackageName = pn;
+        mCallingUid = uid;
+        mFocusGainRequest = focusRequest;
+        mGrantFlags = grantFlags;
+        mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE;
+        mFocusLossWasNotified = true;
+        mFocusController = ctlr;
+        mSdkTarget = sdk;
+    }
+
+    FocusRequester(AudioFocusInfo afi, IAudioFocusDispatcher afl,
+             IBinder source, AudioFocusDeathHandler hdlr, @NonNull MediaFocusControl ctlr) {
+        mAttributes = afi.getAttributes();
+        mClientId = afi.getClientId();
+        mPackageName = afi.getPackageName();
+        mCallingUid = afi.getClientUid();
+        mFocusGainRequest = afi.getGainRequest();
+        mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE;
+        mFocusLossWasNotified = true;
+        mGrantFlags = afi.getFlags();
+        mSdkTarget = afi.getSdkTarget();
+
+        mFocusDispatcher = afl;
+        mSourceRef = source;
+        mDeathHandler = hdlr;
+        mFocusController = ctlr;
+    }
+
+    boolean hasSameClient(String otherClient) {
+        return mClientId.compareTo(otherClient) == 0;
+    }
+
+    boolean isLockedFocusOwner() {
+        return ((mGrantFlags & AudioManager.AUDIOFOCUS_FLAG_LOCK) != 0);
+    }
+
+    boolean hasSameBinder(IBinder ib) {
+        return (mSourceRef != null) && mSourceRef.equals(ib);
+    }
+
+    boolean hasSameDispatcher(IAudioFocusDispatcher fd) {
+        return (mFocusDispatcher != null) && mFocusDispatcher.equals(fd);
+    }
+
+    boolean hasSamePackage(@NonNull String pack) {
+        return mPackageName.compareTo(pack) == 0;
+    }
+
+    boolean hasSameUid(int uid) {
+        return mCallingUid == uid;
+    }
+
+    int getClientUid() {
+        return mCallingUid;
+    }
+
+    String getClientId() {
+        return mClientId;
+    }
+
+    int getGainRequest() {
+        return mFocusGainRequest;
+    }
+
+    int getGrantFlags() {
+        return mGrantFlags;
+    }
+
+    AudioAttributes getAudioAttributes() {
+        return mAttributes;
+    }
+
+    int getSdkTarget() {
+        return mSdkTarget;
+    }
+
+    private static String focusChangeToString(int focus) {
+        switch(focus) {
+            case AudioManager.AUDIOFOCUS_NONE:
+                return "none";
+            case AudioManager.AUDIOFOCUS_GAIN:
+                return "GAIN";
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
+                return "GAIN_TRANSIENT";
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
+                return "GAIN_TRANSIENT_MAY_DUCK";
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
+                return "GAIN_TRANSIENT_EXCLUSIVE";
+            case AudioManager.AUDIOFOCUS_LOSS:
+                return "LOSS";
+            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                return "LOSS_TRANSIENT";
+            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                return "LOSS_TRANSIENT_CAN_DUCK";
+            default:
+                return "[invalid focus change" + focus + "]";
+        }
+    }
+
+    private String focusGainToString() {
+        return focusChangeToString(mFocusGainRequest);
+    }
+
+    private String focusLossToString() {
+        return focusChangeToString(mFocusLossReceived);
+    }
+
+    private static String flagsToString(int flags) {
+        String msg = new String();
+        if ((flags & AudioManager.AUDIOFOCUS_FLAG_DELAY_OK) != 0) {
+            msg += "DELAY_OK";
+        }
+        if ((flags & AudioManager.AUDIOFOCUS_FLAG_LOCK) != 0)     {
+            if (!msg.isEmpty()) { msg += "|"; }
+            msg += "LOCK";
+        }
+        if ((flags & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0) {
+            if (!msg.isEmpty()) { msg += "|"; }
+            msg += "PAUSES_ON_DUCKABLE_LOSS";
+        }
+        return msg;
+    }
+
+    void dump(PrintWriter pw) {
+        pw.println("  source:" + mSourceRef
+                + " -- pack: " + mPackageName
+                + " -- client: " + mClientId
+                + " -- gain: " + focusGainToString()
+                + " -- flags: " + flagsToString(mGrantFlags)
+                + " -- loss: " + focusLossToString()
+                + " -- notified: " + mFocusLossWasNotified
+                + " -- uid: " + mCallingUid
+                + " -- attr: " + mAttributes
+                + " -- sdk:" + mSdkTarget);
+    }
+
+
+    void release() {
+        final IBinder srcRef = mSourceRef;
+        final AudioFocusDeathHandler deathHdlr = mDeathHandler;
+        try {
+            if (srcRef != null && deathHdlr != null) {
+                srcRef.unlinkToDeath(deathHdlr, 0);
+            }
+        } catch (java.util.NoSuchElementException e) { }
+        mDeathHandler = null;
+        mFocusDispatcher = null;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        release();
+        super.finalize();
+    }
+
+    /**
+     * For a given audio focus gain request, return the audio focus loss type that will result
+     * from it, taking into account any previous focus loss.
+     * @param gainRequest
+     * @return the audio focus loss type that matches the gain request
+     */
+    private int focusLossForGainRequest(int gainRequest) {
+        switch(gainRequest) {
+            case AudioManager.AUDIOFOCUS_GAIN:
+                switch(mFocusLossReceived) {
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                    case AudioManager.AUDIOFOCUS_LOSS:
+                    case AudioManager.AUDIOFOCUS_NONE:
+                        return AudioManager.AUDIOFOCUS_LOSS;
+                }
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
+                switch(mFocusLossReceived) {
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                    case AudioManager.AUDIOFOCUS_NONE:
+                        return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
+                    case AudioManager.AUDIOFOCUS_LOSS:
+                        return AudioManager.AUDIOFOCUS_LOSS;
+                }
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
+                switch(mFocusLossReceived) {
+                    case AudioManager.AUDIOFOCUS_NONE:
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                        return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                        return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
+                    case AudioManager.AUDIOFOCUS_LOSS:
+                        return AudioManager.AUDIOFOCUS_LOSS;
+                }
+            default:
+                Log.e(TAG, "focusLossForGainRequest() for invalid focus request "+ gainRequest);
+                        return AudioManager.AUDIOFOCUS_NONE;
+        }
+    }
+
+    /**
+     * Handle the loss of focus resulting from a given focus gain.
+     * @param focusGain the focus gain from which the loss of focus is resulting
+     * @param frWinner the new focus owner
+     * @return true if the focus loss is definitive, false otherwise.
+     */
+    @GuardedBy("MediaFocusControl.mAudioFocusLock")
+    boolean handleFocusLossFromGain(int focusGain, final FocusRequester frWinner, boolean forceDuck)
+    {
+        final int focusLoss = focusLossForGainRequest(focusGain);
+        handleFocusLoss(focusLoss, frWinner, forceDuck);
+        return (focusLoss == AudioManager.AUDIOFOCUS_LOSS);
+    }
+
+    @GuardedBy("MediaFocusControl.mAudioFocusLock")
+    void handleFocusGain(int focusGain) {
+        try {
+            mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE;
+            mFocusController.notifyExtPolicyFocusGrant_syncAf(toAudioFocusInfo(),
+                    AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+            final IAudioFocusDispatcher fd = mFocusDispatcher;
+            if (fd != null) {
+                if (DEBUG) {
+                    Log.v(TAG, "dispatching " + focusChangeToString(focusGain) + " to "
+                        + mClientId);
+                }
+                if (mFocusLossWasNotified) {
+                    fd.dispatchAudioFocusChange(focusGain, mClientId);
+                }
+            }
+            mFocusController.unduckPlayers(this);
+        } catch (android.os.RemoteException e) {
+            Log.e(TAG, "Failure to signal gain of audio focus due to: ", e);
+        }
+    }
+
+    @GuardedBy("MediaFocusControl.mAudioFocusLock")
+    void handleFocusGainFromRequest(int focusRequestResult) {
+        if (focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            mFocusController.unduckPlayers(this);
+        }
+    }
+
+    @GuardedBy("MediaFocusControl.mAudioFocusLock")
+    void handleFocusLoss(int focusLoss, @Nullable final FocusRequester frWinner, boolean forceDuck)
+    {
+        try {
+            if (focusLoss != mFocusLossReceived) {
+                mFocusLossReceived = focusLoss;
+                mFocusLossWasNotified = false;
+                // before dispatching a focus loss, check if the following conditions are met:
+                // 1/ the framework is not supposed to notify the focus loser on a DUCK loss
+                //    (i.e. it has a focus controller that implements a ducking policy)
+                // 2/ it is a DUCK loss
+                // 3/ the focus loser isn't flagged as pausing in a DUCK loss
+                // if they are, do not notify the focus loser
+                if (!mFocusController.mustNotifyFocusOwnerOnDuck()
+                        && mFocusLossReceived == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
+                        && (mGrantFlags
+                                & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) == 0) {
+                    if (DEBUG) {
+                        Log.v(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived)
+                                + " to " + mClientId + ", to be handled externally");
+                    }
+                    mFocusController.notifyExtPolicyFocusLoss_syncAf(
+                            toAudioFocusInfo(), false /* wasDispatched */);
+                    return;
+                }
+
+                // check enforcement by the framework
+                boolean handled = false;
+                if (frWinner != null) {
+                    handled = frameworkHandleFocusLoss(focusLoss, frWinner, forceDuck);
+                }
+
+                if (handled) {
+                    if (DEBUG) {
+                        Log.v(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived)
+                            + " to " + mClientId + ", ducking implemented by framework");
+                    }
+                    mFocusController.notifyExtPolicyFocusLoss_syncAf(
+                            toAudioFocusInfo(), false /* wasDispatched */);
+                    return; // with mFocusLossWasNotified = false
+                }
+
+                final IAudioFocusDispatcher fd = mFocusDispatcher;
+                if (fd != null) {
+                    if (DEBUG) {
+                        Log.v(TAG, "dispatching " + focusChangeToString(mFocusLossReceived) + " to "
+                            + mClientId);
+                    }
+                    mFocusController.notifyExtPolicyFocusLoss_syncAf(
+                            toAudioFocusInfo(), true /* wasDispatched */);
+                    mFocusLossWasNotified = true;
+                    fd.dispatchAudioFocusChange(mFocusLossReceived, mClientId);
+                }
+            }
+        } catch (android.os.RemoteException e) {
+            Log.e(TAG, "Failure to signal loss of audio focus due to:", e);
+        }
+    }
+
+    /**
+     * Let the framework handle the focus loss if possible
+     * @param focusLoss
+     * @param frWinner
+     * @param forceDuck
+     * @return true if the framework handled the focus loss
+     */
+    @GuardedBy("MediaFocusControl.mAudioFocusLock")
+    private boolean frameworkHandleFocusLoss(int focusLoss, @NonNull final FocusRequester frWinner,
+                                             boolean forceDuck) {
+        if (frWinner.mCallingUid == this.mCallingUid) {
+            // the focus change is within the same app, so let the dispatching
+            // happen as if the framework was not involved.
+            return false;
+        }
+
+        if (focusLoss == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+            if (!MediaFocusControl.ENFORCE_DUCKING) {
+                return false;
+            }
+
+            // candidate for enforcement by the framework
+            if (!forceDuck && ((mGrantFlags
+                    & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0)) {
+                // the focus loser declared it would pause instead of duck, let it
+                // handle it (the framework doesn't pause for apps)
+                Log.v(TAG, "not ducking uid " + this.mCallingUid + " - flags");
+                return false;
+            }
+            if (!forceDuck && (MediaFocusControl.ENFORCE_DUCKING_FOR_NEW
+                    && this.getSdkTarget() <= MediaFocusControl.DUCKING_IN_APP_SDK_LEVEL)) {
+                // legacy behavior, apps used to be notified when they should be ducking
+                Log.v(TAG, "not ducking uid " + this.mCallingUid + " - old SDK");
+                return false;
+            }
+
+            return mFocusController.duckPlayers(frWinner, this, forceDuck);
+        }
+        return false;
+    }
+
+    int dispatchFocusChange(int focusChange) {
+        final IAudioFocusDispatcher fd = mFocusDispatcher;
+        if (fd == null) {
+            if (MediaFocusControl.DEBUG) { Log.e(TAG, "dispatchFocusChange: no focus dispatcher"); }
+            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        }
+        if (focusChange == AudioManager.AUDIOFOCUS_NONE) {
+            if (MediaFocusControl.DEBUG) { Log.v(TAG, "dispatchFocusChange: AUDIOFOCUS_NONE"); }
+            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        } else if ((focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN)
+                && (mFocusGainRequest != focusChange)){
+            Log.w(TAG, "focus gain was requested with " + mFocusGainRequest
+                    + ", dispatching " + focusChange);
+        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
+                || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
+                || focusChange == AudioManager.AUDIOFOCUS_LOSS) {
+            mFocusLossReceived = focusChange;
+        }
+        try {
+            fd.dispatchAudioFocusChange(focusChange, mClientId);
+        } catch (android.os.RemoteException e) {
+            Log.e(TAG, "dispatchFocusChange: error talking to focus listener " + mClientId, e);
+            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        }
+        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+    }
+
+    void dispatchFocusResultFromExtPolicy(int requestResult) {
+        final IAudioFocusDispatcher fd = mFocusDispatcher;
+        if (fd == null) {
+            if (MediaFocusControl.DEBUG) {
+                Log.e(TAG, "dispatchFocusResultFromExtPolicy: no focus dispatcher");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "dispatching result" + requestResult + " to " + mClientId);
+        }
+        try {
+            fd.dispatchFocusResultFromExtPolicy(requestResult, mClientId);
+        } catch (android.os.RemoteException e) {
+            Log.e(TAG, "dispatchFocusResultFromExtPolicy: error talking to focus listener"
+                    + mClientId, e);
+        }
+    }
+
+    AudioFocusInfo toAudioFocusInfo() {
+        return new AudioFocusInfo(mAttributes, mCallingUid, mClientId, mPackageName,
+                mFocusGainRequest, mFocusLossReceived, mGrantFlags, mSdkTarget);
+    }
+}
diff --git a/com/android/server/audio/MediaFocusControl.java b/com/android/server/audio/MediaFocusControl.java
new file mode 100644
index 0000000..26281b7
--- /dev/null
+++ b/com/android/server/audio/MediaFocusControl.java
@@ -0,0 +1,1110 @@
+/*
+ * Copyright (C) 2013 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFocusInfo;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.IAudioFocusDispatcher;
+import android.media.MediaMetrics;
+import android.media.audiopolicy.IAudioPolicyCallback;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * @hide
+ *
+ */
+public class MediaFocusControl implements PlayerFocusEnforcer {
+
+    private static final String TAG = "MediaFocusControl";
+    static final boolean DEBUG = false;
+
+    /**
+     * set to true so the framework enforces ducking itself, without communicating to apps
+     * that they lost focus for most use cases.
+     */
+    static final boolean ENFORCE_DUCKING = true;
+    /**
+     * set to true to the framework enforces ducking itself only with apps above a given SDK
+     * target level. Is ignored if ENFORCE_DUCKING is false.
+     */
+    static final boolean ENFORCE_DUCKING_FOR_NEW = true;
+    /**
+     * the SDK level (included) up to which the framework doesn't enforce ducking itself. Is ignored
+     * if ENFORCE_DUCKING_FOR_NEW is false;
+     */
+    // automatic ducking was introduced for Android O
+    static final int DUCKING_IN_APP_SDK_LEVEL = Build.VERSION_CODES.N_MR1;
+    /**
+     * set to true so the framework enforces muting media/game itself when the device is ringing
+     * or in a call.
+     */
+    static final boolean ENFORCE_MUTING_FOR_RING_OR_CALL = true;
+
+    private final Context mContext;
+    private final AppOpsManager mAppOps;
+    private PlayerFocusEnforcer mFocusEnforcer; // never null
+    private boolean mMultiAudioFocusEnabled = false;
+
+    private boolean mRingOrCallActive = false;
+
+    private final Object mExtFocusChangeLock = new Object();
+    @GuardedBy("mExtFocusChangeLock")
+    private long mExtFocusChangeCounter;
+
+    protected MediaFocusControl(Context cntxt, PlayerFocusEnforcer pfe) {
+        mContext = cntxt;
+        mAppOps = (AppOpsManager)mContext.getSystemService(Context.APP_OPS_SERVICE);
+        mFocusEnforcer = pfe;
+        mMultiAudioFocusEnabled = Settings.System.getInt(mContext.getContentResolver(),
+                Settings.System.MULTI_AUDIO_FOCUS_ENABLED, 0) != 0;
+    }
+
+    protected void dump(PrintWriter pw) {
+        pw.println("\nMediaFocusControl dump time: "
+                + DateFormat.getTimeInstance().format(new Date()));
+        dumpFocusStack(pw);
+        pw.println("\n");
+        // log
+        mEventLogger.dump(pw);
+        dumpMultiAudioFocus(pw);
+    }
+
+    //=================================================================
+    // PlayerFocusEnforcer implementation
+    @Override
+    public boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser,
+                               boolean forceDuck) {
+        return mFocusEnforcer.duckPlayers(winner, loser, forceDuck);
+    }
+
+    @Override
+    public void unduckPlayers(@NonNull FocusRequester winner) {
+        mFocusEnforcer.unduckPlayers(winner);
+    }
+
+    @Override
+    public void mutePlayersForCall(int[] usagesToMute) {
+        mFocusEnforcer.mutePlayersForCall(usagesToMute);
+    }
+
+    @Override
+    public void unmutePlayersForCall() {
+        mFocusEnforcer.unmutePlayersForCall();
+    }
+
+    //==========================================================================================
+    // AudioFocus
+    //==========================================================================================
+
+    private final static Object mAudioFocusLock = new Object();
+
+    /**
+     * Arbitrary maximum size of audio focus stack to prevent apps OOM'ing this process.
+     */
+    private static final int MAX_STACK_SIZE = 100;
+
+    private static final AudioEventLogger mEventLogger = new AudioEventLogger(50,
+            "focus commands as seen by MediaFocusControl");
+
+    private static final String mMetricsId = MediaMetrics.Name.AUDIO_FOCUS;
+
+    /*package*/ void noFocusForSuspendedApp(@NonNull String packageName, int uid) {
+        synchronized (mAudioFocusLock) {
+            final Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
+            List<String> clientsToRemove = new ArrayList<>();
+            while (stackIterator.hasNext()) {
+                final FocusRequester focusOwner = stackIterator.next();
+                if (focusOwner.hasSameUid(uid) && focusOwner.hasSamePackage(packageName)) {
+                    clientsToRemove.add(focusOwner.getClientId());
+                    mEventLogger.log((new AudioEventLogger.StringEvent(
+                            "focus owner:" + focusOwner.getClientId()
+                                    + " in uid:" + uid + " pack: " + packageName
+                                    + " getting AUDIOFOCUS_LOSS due to app suspension"))
+                            .printLog(TAG));
+                    // make the suspended app lose focus through its focus listener (if any)
+                    focusOwner.dispatchFocusChange(AudioManager.AUDIOFOCUS_LOSS);
+                }
+            }
+            for (String clientToRemove : clientsToRemove) {
+                // update the stack but don't signal the change.
+                removeFocusStackEntry(clientToRemove, false, true);
+            }
+        }
+    }
+
+    /*package*/ boolean hasAudioFocusUsers() {
+        synchronized (mAudioFocusLock) {
+            return !mFocusStack.empty();
+        }
+    }
+
+    /**
+     * Discard the current audio focus owner.
+     * Notify top of audio focus stack that it lost focus (regardless of possibility to reassign
+     * focus), remove it from the stack, and clear the remote control display.
+     */
+    protected void discardAudioFocusOwner() {
+        synchronized(mAudioFocusLock) {
+            if (!mFocusStack.empty()) {
+                // notify the current focus owner it lost focus after removing it from stack
+                final FocusRequester exFocusOwner = mFocusStack.pop();
+                exFocusOwner.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null,
+                        false /*forceDuck*/);
+                exFocusOwner.release();
+            }
+        }
+    }
+
+    @GuardedBy("mAudioFocusLock")
+    private void notifyTopOfAudioFocusStack() {
+        // notify the top of the stack it gained focus
+        if (!mFocusStack.empty()) {
+            if (canReassignAudioFocus()) {
+                mFocusStack.peek().handleFocusGain(AudioManager.AUDIOFOCUS_GAIN);
+            }
+        }
+
+        if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) {
+            for (FocusRequester multifr : mMultiAudioFocusList) {
+                if (isLockedFocusOwner(multifr)) {
+                    multifr.handleFocusGain(AudioManager.AUDIOFOCUS_GAIN);
+                }
+            }
+        }
+    }
+
+    /**
+     * Focus is requested, propagate the associated loss throughout the stack.
+     * Will also remove entries in the stack that have just received a definitive loss of focus.
+     * @param focusGain the new focus gain that will later be added at the top of the stack
+     */
+    @GuardedBy("mAudioFocusLock")
+    private void propagateFocusLossFromGain_syncAf(int focusGain, final FocusRequester fr,
+                                                   boolean forceDuck) {
+        final List<String> clientsToRemove = new LinkedList<String>();
+        // going through the audio focus stack to signal new focus, traversing order doesn't
+        // matter as all entries respond to the same external focus gain
+        if (!mFocusStack.empty()) {
+            for (FocusRequester focusLoser : mFocusStack) {
+                final boolean isDefinitiveLoss =
+                        focusLoser.handleFocusLossFromGain(focusGain, fr, forceDuck);
+                if (isDefinitiveLoss) {
+                    clientsToRemove.add(focusLoser.getClientId());
+                }
+            }
+        }
+
+        if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) {
+            for (FocusRequester multifocusLoser : mMultiAudioFocusList) {
+                final boolean isDefinitiveLoss =
+                        multifocusLoser.handleFocusLossFromGain(focusGain, fr, forceDuck);
+                if (isDefinitiveLoss) {
+                    clientsToRemove.add(multifocusLoser.getClientId());
+                }
+            }
+        }
+
+        for (String clientToRemove : clientsToRemove) {
+            removeFocusStackEntry(clientToRemove, false /*signal*/,
+                    true /*notifyFocusFollowers*/);
+        }
+    }
+
+    private final Stack<FocusRequester> mFocusStack = new Stack<FocusRequester>();
+
+    ArrayList<FocusRequester> mMultiAudioFocusList = new ArrayList<FocusRequester>();
+
+    /**
+     * Helper function:
+     * Display in the log the current entries in the audio focus stack
+     */
+    private void dumpFocusStack(PrintWriter pw) {
+        pw.println("\nAudio Focus stack entries (last is top of stack):");
+        synchronized(mAudioFocusLock) {
+            Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
+            while(stackIterator.hasNext()) {
+                stackIterator.next().dump(pw);
+            }
+            pw.println("\n");
+            if (mFocusPolicy == null) {
+                pw.println("No external focus policy\n");
+            } else {
+                pw.println("External focus policy: "+ mFocusPolicy + ", focus owners:\n");
+                dumpExtFocusPolicyFocusOwners(pw);
+            }
+        }
+        pw.println("\n");
+        pw.println(" Notify on duck:  " + mNotifyFocusOwnerOnDuck + "\n");
+        pw.println(" In ring or call: " + mRingOrCallActive + "\n");
+    }
+
+    /**
+     * Remove a focus listener from the focus stack.
+     * @param clientToRemove the focus listener
+     * @param signal if true and the listener was at the top of the focus stack, i.e. it was holding
+     *   focus, notify the next item in the stack it gained focus.
+     */
+    @GuardedBy("mAudioFocusLock")
+    private void removeFocusStackEntry(String clientToRemove, boolean signal,
+            boolean notifyFocusFollowers) {
+        // is the current top of the focus stack abandoning focus? (because of request, not death)
+        if (!mFocusStack.empty() && mFocusStack.peek().hasSameClient(clientToRemove))
+        {
+            //Log.i(TAG, "   removeFocusStackEntry() removing top of stack");
+            FocusRequester fr = mFocusStack.pop();
+            fr.release();
+            if (notifyFocusFollowers) {
+                final AudioFocusInfo afi = fr.toAudioFocusInfo();
+                afi.clearLossReceived();
+                notifyExtPolicyFocusLoss_syncAf(afi, false);
+            }
+            if (signal) {
+                // notify the new top of the stack it gained focus
+                notifyTopOfAudioFocusStack();
+            }
+        } else {
+            // focus is abandoned by a client that's not at the top of the stack,
+            // no need to update focus.
+            // (using an iterator on the stack so we can safely remove an entry after having
+            //  evaluated it, traversal order doesn't matter here)
+            Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
+            while(stackIterator.hasNext()) {
+                FocusRequester fr = stackIterator.next();
+                if(fr.hasSameClient(clientToRemove)) {
+                    Log.i(TAG, "AudioFocus  removeFocusStackEntry(): removing entry for "
+                            + clientToRemove);
+                    stackIterator.remove();
+                    // stack entry not used anymore, clear references
+                    fr.release();
+                }
+            }
+        }
+
+        if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) {
+            Iterator<FocusRequester> listIterator = mMultiAudioFocusList.iterator();
+            while (listIterator.hasNext()) {
+                FocusRequester fr = listIterator.next();
+                if (fr.hasSameClient(clientToRemove)) {
+                    listIterator.remove();
+                    fr.release();
+                }
+            }
+
+            if (signal) {
+                // notify the new top of the stack it gained focus
+                notifyTopOfAudioFocusStack();
+            }
+        }
+    }
+
+    /**
+     * Remove focus listeners from the focus stack for a particular client when it has died.
+     */
+    @GuardedBy("mAudioFocusLock")
+    private void removeFocusStackEntryOnDeath(IBinder cb) {
+        // is the owner of the audio focus part of the client to remove?
+        boolean isTopOfStackForClientToRemove = !mFocusStack.isEmpty() &&
+                mFocusStack.peek().hasSameBinder(cb);
+        // (using an iterator on the stack so we can safely remove an entry after having
+        //  evaluated it, traversal order doesn't matter here)
+        Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
+        while(stackIterator.hasNext()) {
+            FocusRequester fr = stackIterator.next();
+            if(fr.hasSameBinder(cb)) {
+                Log.i(TAG, "AudioFocus  removeFocusStackEntryOnDeath(): removing entry for " + cb);
+                stackIterator.remove();
+                // stack entry not used anymore, clear references
+                fr.release();
+            }
+        }
+        if (isTopOfStackForClientToRemove) {
+            // we removed an entry at the top of the stack:
+            //  notify the new top of the stack it gained focus.
+            notifyTopOfAudioFocusStack();
+        }
+    }
+
+    /**
+     * Helper function for external focus policy:
+     * Remove focus listeners from the list of potential focus owners for a particular client when
+     * it has died.
+     */
+    @GuardedBy("mAudioFocusLock")
+    private void removeFocusEntryForExtPolicy(IBinder cb) {
+        if (mFocusOwnersForFocusPolicy.isEmpty()) {
+            return;
+        }
+        boolean released = false;
+        final Set<Entry<String, FocusRequester>> owners = mFocusOwnersForFocusPolicy.entrySet();
+        final Iterator<Entry<String, FocusRequester>> ownerIterator = owners.iterator();
+        while (ownerIterator.hasNext()) {
+            final Entry<String, FocusRequester> owner = ownerIterator.next();
+            final FocusRequester fr = owner.getValue();
+            if (fr.hasSameBinder(cb)) {
+                ownerIterator.remove();
+                fr.release();
+                notifyExtFocusPolicyFocusAbandon_syncAf(fr.toAudioFocusInfo());
+                break;
+            }
+        }
+    }
+
+    /**
+     * Helper function:
+     * Returns true if the system is in a state where the focus can be reevaluated, false otherwise.
+     * The implementation guarantees that a state where focus cannot be immediately reassigned
+     * implies that an "locked" focus owner is at the top of the focus stack.
+     * Modifications to the implementation that break this assumption will cause focus requests to
+     * misbehave when honoring the AudioManager.AUDIOFOCUS_FLAG_DELAY_OK flag.
+     */
+    private boolean canReassignAudioFocus() {
+        // focus requests are rejected during a phone call or when the phone is ringing
+        // this is equivalent to IN_VOICE_COMM_FOCUS_ID having the focus
+        if (!mFocusStack.isEmpty() && isLockedFocusOwner(mFocusStack.peek())) {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean isLockedFocusOwner(FocusRequester fr) {
+        return (fr.hasSameClient(AudioSystem.IN_VOICE_COMM_FOCUS_ID) || fr.isLockedFocusOwner());
+    }
+
+    /**
+     * Helper function
+     * Pre-conditions: focus stack is not empty, there is one or more locked focus owner
+     *                 at the top of the focus stack
+     * Push the focus requester onto the audio focus stack at the first position immediately
+     * following the locked focus owners.
+     * @return {@link AudioManager#AUDIOFOCUS_REQUEST_GRANTED} or
+     *     {@link AudioManager#AUDIOFOCUS_REQUEST_DELAYED}
+     */
+    @GuardedBy("mAudioFocusLock")
+    private int pushBelowLockedFocusOwners(FocusRequester nfr) {
+        int lastLockedFocusOwnerIndex = mFocusStack.size();
+        for (int index = mFocusStack.size()-1; index >= 0; index--) {
+            if (isLockedFocusOwner(mFocusStack.elementAt(index))) {
+                lastLockedFocusOwnerIndex = index;
+            }
+        }
+        if (lastLockedFocusOwnerIndex == mFocusStack.size()) {
+            // this should not happen, but handle it and log an error
+            Log.e(TAG, "No exclusive focus owner found in propagateFocusLossFromGain_syncAf()",
+                    new Exception());
+            // no exclusive owner, push at top of stack, focus is granted, propagate change
+            propagateFocusLossFromGain_syncAf(nfr.getGainRequest(), nfr, false /*forceDuck*/);
+            mFocusStack.push(nfr);
+            return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+        } else {
+            mFocusStack.insertElementAt(nfr, lastLockedFocusOwnerIndex);
+            return AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
+        }
+    }
+
+    /**
+     * Inner class to monitor audio focus client deaths, and remove them from the audio focus
+     * stack if necessary.
+     */
+    protected class AudioFocusDeathHandler implements IBinder.DeathRecipient {
+        private IBinder mCb; // To be notified of client's death
+
+        AudioFocusDeathHandler(IBinder cb) {
+            mCb = cb;
+        }
+
+        public void binderDied() {
+            synchronized(mAudioFocusLock) {
+                if (mFocusPolicy != null) {
+                    removeFocusEntryForExtPolicy(mCb);
+                } else {
+                    removeFocusStackEntryOnDeath(mCb);
+                    if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) {
+                        Iterator<FocusRequester> listIterator = mMultiAudioFocusList.iterator();
+                        while (listIterator.hasNext()) {
+                            FocusRequester fr = listIterator.next();
+                            if (fr.hasSameBinder(mCb)) {
+                                listIterator.remove();
+                                fr.release();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Indicates whether to notify an audio focus owner when it loses focus
+     * with {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK} if it will only duck.
+     * This variable being false indicates an AudioPolicy has been registered and has signaled
+     * it will handle audio ducking.
+     */
+    private boolean mNotifyFocusOwnerOnDuck = true;
+
+    protected void setDuckingInExtPolicyAvailable(boolean available) {
+        mNotifyFocusOwnerOnDuck = !available;
+    }
+
+    boolean mustNotifyFocusOwnerOnDuck() { return mNotifyFocusOwnerOnDuck; }
+
+    private ArrayList<IAudioPolicyCallback> mFocusFollowers = new ArrayList<IAudioPolicyCallback>();
+
+    void addFocusFollower(IAudioPolicyCallback ff) {
+        if (ff == null) {
+            return;
+        }
+        synchronized(mAudioFocusLock) {
+            boolean found = false;
+            for (IAudioPolicyCallback pcb : mFocusFollowers) {
+                if (pcb.asBinder().equals(ff.asBinder())) {
+                    found = true;
+                    break;
+                }
+            }
+            if (found) {
+                return;
+            } else {
+                mFocusFollowers.add(ff);
+                notifyExtPolicyCurrentFocusAsync(ff);
+            }
+        }
+    }
+
+    void removeFocusFollower(IAudioPolicyCallback ff) {
+        if (ff == null) {
+            return;
+        }
+        synchronized(mAudioFocusLock) {
+            for (IAudioPolicyCallback pcb : mFocusFollowers) {
+                if (pcb.asBinder().equals(ff.asBinder())) {
+                    mFocusFollowers.remove(pcb);
+                    break;
+                }
+            }
+        }
+    }
+
+    /** The current audio focus policy */
+    @GuardedBy("mAudioFocusLock")
+    @Nullable private IAudioPolicyCallback mFocusPolicy = null;
+    /**
+     * The audio focus policy that was registered before a test focus policy was registered
+     * during a test
+     */
+    @GuardedBy("mAudioFocusLock")
+    @Nullable private IAudioPolicyCallback mPreviousFocusPolicy = null;
+
+    // Since we don't have a stack of focus owners when using an external focus policy, we keep
+    // track of all the focus requesters in this map, with their clientId as the key. This is
+    // used both for focus dispatch and death handling
+    private HashMap<String, FocusRequester> mFocusOwnersForFocusPolicy =
+            new HashMap<String, FocusRequester>();
+
+    void setFocusPolicy(IAudioPolicyCallback policy, boolean isTestFocusPolicy) {
+        if (policy == null) {
+            return;
+        }
+        synchronized (mAudioFocusLock) {
+            if (isTestFocusPolicy) {
+                mPreviousFocusPolicy = mFocusPolicy;
+            }
+            mFocusPolicy = policy;
+        }
+    }
+
+    void unsetFocusPolicy(IAudioPolicyCallback policy, boolean isTestFocusPolicy) {
+        if (policy == null) {
+            return;
+        }
+        synchronized (mAudioFocusLock) {
+            if (mFocusPolicy == policy) {
+                if (isTestFocusPolicy) {
+                    // restore the focus policy that was there before the focus policy test started
+                    mFocusPolicy = mPreviousFocusPolicy;
+                } else {
+                    mFocusPolicy = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * @param pcb non null
+     */
+    void notifyExtPolicyCurrentFocusAsync(IAudioPolicyCallback pcb) {
+        final IAudioPolicyCallback pcb2 = pcb;
+        final Thread thread = new Thread() {
+            @Override
+            public void run() {
+                synchronized(mAudioFocusLock) {
+                    if (mFocusStack.isEmpty()) {
+                        return;
+                    }
+                    try {
+                        pcb2.notifyAudioFocusGrant(mFocusStack.peek().toAudioFocusInfo(),
+                                // top of focus stack always has focus
+                                AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "Can't call notifyAudioFocusGrant() on IAudioPolicyCallback "
+                                + pcb2.asBinder(), e);
+                    }
+                }
+            }
+        };
+        thread.start();
+    }
+
+    /**
+     * Called synchronized on mAudioFocusLock
+     */
+    void notifyExtPolicyFocusGrant_syncAf(AudioFocusInfo afi, int requestResult) {
+        for (IAudioPolicyCallback pcb : mFocusFollowers) {
+            try {
+                // oneway
+                pcb.notifyAudioFocusGrant(afi, requestResult);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Can't call notifyAudioFocusGrant() on IAudioPolicyCallback "
+                        + pcb.asBinder(), e);
+            }
+        }
+    }
+
+    /**
+     * Called synchronized on mAudioFocusLock
+     */
+    void notifyExtPolicyFocusLoss_syncAf(AudioFocusInfo afi, boolean wasDispatched) {
+        for (IAudioPolicyCallback pcb : mFocusFollowers) {
+            try {
+                // oneway
+                pcb.notifyAudioFocusLoss(afi, wasDispatched);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Can't call notifyAudioFocusLoss() on IAudioPolicyCallback "
+                        + pcb.asBinder(), e);
+            }
+        }
+    }
+
+    /**
+     * Called synchronized on mAudioFocusLock.
+     * Can only be called with an external focus policy installed (mFocusPolicy != null)
+     * @param afi
+     * @param fd
+     * @param cb binder of the focus requester
+     * @return true if the external audio focus policy (if any) can handle the focus request,
+     *     and false if there was any error handling the request (e.g. error talking to policy,
+     *     focus requester is already dead)
+     */
+    boolean notifyExtFocusPolicyFocusRequest_syncAf(AudioFocusInfo afi,
+            IAudioFocusDispatcher fd, @NonNull IBinder cb) {
+        if (DEBUG) {
+            Log.v(TAG, "notifyExtFocusPolicyFocusRequest client="+afi.getClientId()
+            + " dispatcher=" + fd);
+        }
+        synchronized (mExtFocusChangeLock) {
+            afi.setGen(mExtFocusChangeCounter++);
+        }
+        final FocusRequester existingFr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
+        boolean keepTrack = false;
+        if (existingFr != null) {
+            if (!existingFr.hasSameDispatcher(fd)) {
+                existingFr.release();
+                keepTrack = true;
+            }
+        } else {
+            keepTrack = true;
+        }
+        if (keepTrack) {
+            final AudioFocusDeathHandler hdlr = new AudioFocusDeathHandler(cb);
+            try {
+                cb.linkToDeath(hdlr, 0);
+            } catch (RemoteException e) {
+                // client has already died!
+                return false;
+            }
+            // new focus (future) focus owner to keep track of
+            mFocusOwnersForFocusPolicy.put(afi.getClientId(),
+                    new FocusRequester(afi, fd, cb, hdlr, this));
+        }
+
+        try {
+            //oneway
+            mFocusPolicy.notifyAudioFocusRequest(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+            return true;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Can't call notifyAudioFocusRequest() on IAudioPolicyCallback "
+                    + mFocusPolicy.asBinder(), e);
+        }
+        return false;
+    }
+
+    void setFocusRequestResultFromExtPolicy(AudioFocusInfo afi, int requestResult) {
+        synchronized (mExtFocusChangeLock) {
+            if (afi.getGen() > mExtFocusChangeCounter) {
+                return;
+            }
+        }
+        final FocusRequester fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
+        if (fr != null) {
+            fr.dispatchFocusResultFromExtPolicy(requestResult);
+        }
+    }
+
+    /**
+     * Called synchronized on mAudioFocusLock
+     * @param afi
+     * @return true if the external audio focus policy (if any) is handling the focus request
+     */
+    boolean notifyExtFocusPolicyFocusAbandon_syncAf(AudioFocusInfo afi) {
+        if (mFocusPolicy == null) {
+            return false;
+        }
+        final FocusRequester fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId());
+        if (fr != null) {
+            fr.release();
+        }
+        try {
+            //oneway
+            mFocusPolicy.notifyAudioFocusAbandon(afi);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Can't call notifyAudioFocusAbandon() on IAudioPolicyCallback "
+                    + mFocusPolicy.asBinder(), e);
+        }
+        return true;
+    }
+
+    /** see AudioManager.dispatchFocusChange(AudioFocusInfo afi, int focusChange, AudioPolicy ap) */
+    int dispatchFocusChange(AudioFocusInfo afi, int focusChange) {
+        if (DEBUG) {
+            Log.v(TAG, "dispatchFocusChange " + focusChange + " to afi client="
+                    + afi.getClientId());
+        }
+        synchronized (mAudioFocusLock) {
+            if (mFocusPolicy == null) {
+                if (DEBUG) { Log.v(TAG, "> failed: no focus policy" ); }
+                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+            }
+            final FocusRequester fr;
+            if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
+                fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId());
+            } else {
+                fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
+            }
+            if (fr == null) {
+                if (DEBUG) { Log.v(TAG, "> failed: no such focus requester known" ); }
+                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+            }
+            return fr.dispatchFocusChange(focusChange);
+        }
+    }
+
+    private void dumpExtFocusPolicyFocusOwners(PrintWriter pw) {
+        final Set<Entry<String, FocusRequester>> owners = mFocusOwnersForFocusPolicy.entrySet();
+        final Iterator<Entry<String, FocusRequester>> ownerIterator = owners.iterator();
+        while (ownerIterator.hasNext()) {
+            final Entry<String, FocusRequester> owner = ownerIterator.next();
+            final FocusRequester fr = owner.getValue();
+            fr.dump(pw);
+        }
+    }
+
+    protected int getCurrentAudioFocus() {
+        synchronized(mAudioFocusLock) {
+            if (mFocusStack.empty()) {
+                return AudioManager.AUDIOFOCUS_NONE;
+            } else {
+                return mFocusStack.peek().getGainRequest();
+            }
+        }
+    }
+
+    /**
+     * Delay after entering ringing or call mode after which the framework will mute streams
+     * that are still playing.
+     */
+    private static final int RING_CALL_MUTING_ENFORCEMENT_DELAY_MS = 100;
+
+    /**
+     * Usages to mute when the device rings or is in a call
+     */
+    private final static int[] USAGES_TO_MUTE_IN_RING_OR_CALL =
+        { AudioAttributes.USAGE_MEDIA, AudioAttributes.USAGE_GAME };
+
+    /**
+     * Return the volume ramp time expected before playback with the given AudioAttributes would
+     * start after gaining audio focus.
+     * @param attr attributes of the sound about to start playing
+     * @return time in ms
+     */
+    protected static int getFocusRampTimeMs(int focusGain, AudioAttributes attr) {
+        switch (attr.getUsage()) {
+            case AudioAttributes.USAGE_MEDIA:
+            case AudioAttributes.USAGE_GAME:
+                return 1000;
+            case AudioAttributes.USAGE_ALARM:
+            case AudioAttributes.USAGE_NOTIFICATION_RINGTONE:
+            case AudioAttributes.USAGE_ASSISTANT:
+            case AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY:
+            case AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
+            case AudioAttributes.USAGE_ANNOUNCEMENT:
+                return 700;
+            case AudioAttributes.USAGE_VOICE_COMMUNICATION:
+            case AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING:
+            case AudioAttributes.USAGE_NOTIFICATION:
+            case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
+            case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
+            case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
+            case AudioAttributes.USAGE_NOTIFICATION_EVENT:
+            case AudioAttributes.USAGE_ASSISTANCE_SONIFICATION:
+            case AudioAttributes.USAGE_VEHICLE_STATUS:
+                return 500;
+            case AudioAttributes.USAGE_EMERGENCY:
+            case AudioAttributes.USAGE_SAFETY:
+            case AudioAttributes.USAGE_UNKNOWN:
+            default:
+                return 0;
+        }
+    }
+
+    /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int)
+     * @param aa
+     * @param focusChangeHint
+     * @param cb
+     * @param fd
+     * @param clientId
+     * @param callingPackageName
+     * @param flags
+     * @param sdk
+     * @param forceDuck only true if
+     *     {@link android.media.AudioFocusRequest.Builder#setFocusGain(int)} was set to true for
+     *                  accessibility.
+     * @return
+     */
+    protected int requestAudioFocus(@NonNull AudioAttributes aa, int focusChangeHint, IBinder cb,
+            IAudioFocusDispatcher fd, @NonNull String clientId, @NonNull String callingPackageName,
+            int flags, int sdk, boolean forceDuck) {
+        new MediaMetrics.Item(mMetricsId)
+                .setUid(Binder.getCallingUid())
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "requestAudioFocus")
+                .set(MediaMetrics.Property.FLAGS, flags)
+                .set(MediaMetrics.Property.FOCUS_CHANGE_HINT,
+                        AudioManager.audioFocusToString(focusChangeHint))
+                //.set(MediaMetrics.Property.SDK, sdk)
+                .record();
+
+        mEventLogger.log((new AudioEventLogger.StringEvent(
+                "requestAudioFocus() from uid/pid " + Binder.getCallingUid()
+                    + "/" + Binder.getCallingPid()
+                    + " clientId=" + clientId + " callingPack=" + callingPackageName
+                    + " req=" + focusChangeHint
+                    + " flags=0x" + Integer.toHexString(flags)
+                    + " sdk=" + sdk))
+                .printLog(TAG));
+        // we need a valid binder callback for clients
+        if (!cb.pingBinder()) {
+            Log.e(TAG, " AudioFocus DOA client for requestAudioFocus(), aborting.");
+            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        }
+
+        if (mAppOps.noteOp(AppOpsManager.OP_TAKE_AUDIO_FOCUS, Binder.getCallingUid(),
+                callingPackageName) != AppOpsManager.MODE_ALLOWED) {
+            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        }
+
+        synchronized(mAudioFocusLock) {
+            if (mFocusStack.size() > MAX_STACK_SIZE) {
+                Log.e(TAG, "Max AudioFocus stack size reached, failing requestAudioFocus()");
+                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+            }
+
+            boolean enteringRingOrCall = !mRingOrCallActive
+                    & (AudioSystem.IN_VOICE_COMM_FOCUS_ID.compareTo(clientId) == 0);
+            if (enteringRingOrCall) { mRingOrCallActive = true; }
+
+            final AudioFocusInfo afiForExtPolicy;
+            if (mFocusPolicy != null) {
+                // construct AudioFocusInfo as it will be communicated to audio focus policy
+                afiForExtPolicy = new AudioFocusInfo(aa, Binder.getCallingUid(),
+                        clientId, callingPackageName, focusChangeHint, 0 /*lossReceived*/,
+                        flags, sdk);
+            } else {
+                afiForExtPolicy = null;
+            }
+
+            // handle delayed focus
+            boolean focusGrantDelayed = false;
+            if (!canReassignAudioFocus()) {
+                if ((flags & AudioManager.AUDIOFOCUS_FLAG_DELAY_OK) == 0) {
+                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+                } else {
+                    // request has AUDIOFOCUS_FLAG_DELAY_OK: focus can't be
+                    // granted right now, so the requester will be inserted in the focus stack
+                    // to receive focus later
+                    focusGrantDelayed = true;
+                }
+            }
+
+            // external focus policy?
+            if (mFocusPolicy != null) {
+                if (notifyExtFocusPolicyFocusRequest_syncAf(afiForExtPolicy, fd, cb)) {
+                    // stop handling focus request here as it is handled by external audio
+                    // focus policy (return code will be handled in AudioManager)
+                    return AudioManager.AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY;
+                } else {
+                    // an error occured, client already dead, bail early
+                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+                }
+            }
+
+            // handle the potential premature death of the new holder of the focus
+            // (premature death == death before abandoning focus)
+            // Register for client death notification
+            AudioFocusDeathHandler afdh = new AudioFocusDeathHandler(cb);
+
+            try {
+                cb.linkToDeath(afdh, 0);
+            } catch (RemoteException e) {
+                // client has already died!
+                Log.w(TAG, "AudioFocus  requestAudioFocus() could not link to "+cb+" binder death");
+                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+            }
+
+            if (!mFocusStack.empty() && mFocusStack.peek().hasSameClient(clientId)) {
+                // if focus is already owned by this client and the reason for acquiring the focus
+                // hasn't changed, don't do anything
+                final FocusRequester fr = mFocusStack.peek();
+                if (fr.getGainRequest() == focusChangeHint && fr.getGrantFlags() == flags) {
+                    // unlink death handler so it can be gc'ed.
+                    // linkToDeath() creates a JNI global reference preventing collection.
+                    cb.unlinkToDeath(afdh, 0);
+                    notifyExtPolicyFocusGrant_syncAf(fr.toAudioFocusInfo(),
+                            AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+                    return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+                }
+                // the reason for the audio focus request has changed: remove the current top of
+                // stack and respond as if we had a new focus owner
+                if (!focusGrantDelayed) {
+                    mFocusStack.pop();
+                    // the entry that was "popped" is the same that was "peeked" above
+                    fr.release();
+                }
+            }
+
+            // focus requester might already be somewhere below in the stack, remove it
+            removeFocusStackEntry(clientId, false /* signal */, false /*notifyFocusFollowers*/);
+
+            final FocusRequester nfr = new FocusRequester(aa, focusChangeHint, flags, fd, cb,
+                    clientId, afdh, callingPackageName, Binder.getCallingUid(), this, sdk);
+
+            if (mMultiAudioFocusEnabled
+                    && (focusChangeHint == AudioManager.AUDIOFOCUS_GAIN)) {
+                if (enteringRingOrCall) {
+                    if (!mMultiAudioFocusList.isEmpty()) {
+                        for (FocusRequester multifr : mMultiAudioFocusList) {
+                            multifr.handleFocusLossFromGain(focusChangeHint, nfr, forceDuck);
+                        }
+                    }
+                } else {
+                    boolean needAdd = true;
+                    if (!mMultiAudioFocusList.isEmpty()) {
+                        for (FocusRequester multifr : mMultiAudioFocusList) {
+                            if (multifr.getClientUid() == Binder.getCallingUid()) {
+                                needAdd = false;
+                                break;
+                            }
+                        }
+                    }
+                    if (needAdd) {
+                        mMultiAudioFocusList.add(nfr);
+                    }
+                    nfr.handleFocusGainFromRequest(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+                    notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(),
+                            AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+                    return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+                }
+            }
+
+            if (focusGrantDelayed) {
+                // focusGrantDelayed being true implies we can't reassign focus right now
+                // which implies the focus stack is not empty.
+                final int requestResult = pushBelowLockedFocusOwners(nfr);
+                if (requestResult != AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
+                    notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(), requestResult);
+                }
+                return requestResult;
+            } else {
+                // propagate the focus change through the stack
+                propagateFocusLossFromGain_syncAf(focusChangeHint, nfr, forceDuck);
+
+                // push focus requester at the top of the audio focus stack
+                mFocusStack.push(nfr);
+                nfr.handleFocusGainFromRequest(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+            }
+            notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(),
+                    AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+
+            if (ENFORCE_MUTING_FOR_RING_OR_CALL & enteringRingOrCall) {
+                runAudioCheckerForRingOrCallAsync(true/*enteringRingOrCall*/);
+            }
+        }//synchronized(mAudioFocusLock)
+
+        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+    }
+
+    /**
+     * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes)
+     * */
+    protected int abandonAudioFocus(IAudioFocusDispatcher fl, String clientId, AudioAttributes aa,
+            String callingPackageName) {
+        new MediaMetrics.Item(mMetricsId)
+                .setUid(Binder.getCallingUid())
+                .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
+                .set(MediaMetrics.Property.CLIENT_NAME, clientId)
+                .set(MediaMetrics.Property.EVENT, "abandonAudioFocus")
+                .record();
+
+        // AudioAttributes are currently ignored, to be used for zones / a11y
+        mEventLogger.log((new AudioEventLogger.StringEvent(
+                "abandonAudioFocus() from uid/pid " + Binder.getCallingUid()
+                    + "/" + Binder.getCallingPid()
+                    + " clientId=" + clientId))
+                .printLog(TAG));
+        try {
+            // this will take care of notifying the new focus owner if needed
+            synchronized(mAudioFocusLock) {
+                // external focus policy?
+                if (mFocusPolicy != null) {
+                    final AudioFocusInfo afi = new AudioFocusInfo(aa, Binder.getCallingUid(),
+                            clientId, callingPackageName, 0 /*gainRequest*/, 0 /*lossReceived*/,
+                            0 /*flags*/, 0 /* sdk n/a here*/);
+                    if (notifyExtFocusPolicyFocusAbandon_syncAf(afi)) {
+                        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+                    }
+                }
+
+                boolean exitingRingOrCall = mRingOrCallActive
+                        & (AudioSystem.IN_VOICE_COMM_FOCUS_ID.compareTo(clientId) == 0);
+                if (exitingRingOrCall) { mRingOrCallActive = false; }
+
+                removeFocusStackEntry(clientId, true /*signal*/, true /*notifyFocusFollowers*/);
+
+                if (ENFORCE_MUTING_FOR_RING_OR_CALL & exitingRingOrCall) {
+                    runAudioCheckerForRingOrCallAsync(false/*enteringRingOrCall*/);
+                }
+            }
+        } catch (java.util.ConcurrentModificationException cme) {
+            // Catching this exception here is temporary. It is here just to prevent
+            // a crash seen when the "Silent" notification is played. This is believed to be fixed
+            // but this try catch block is left just to be safe.
+            Log.e(TAG, "FATAL EXCEPTION AudioFocus  abandonAudioFocus() caused " + cme);
+            cme.printStackTrace();
+        }
+
+        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+    }
+
+
+    protected void unregisterAudioFocusClient(String clientId) {
+        synchronized(mAudioFocusLock) {
+            removeFocusStackEntry(clientId, false, true /*notifyFocusFollowers*/);
+        }
+    }
+
+    private void runAudioCheckerForRingOrCallAsync(final boolean enteringRingOrCall) {
+        new Thread() {
+            public void run() {
+                if (enteringRingOrCall) {
+                    try {
+                        Thread.sleep(RING_CALL_MUTING_ENFORCEMENT_DELAY_MS);
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+                synchronized (mAudioFocusLock) {
+                    // since the new thread starting running the state could have changed, so
+                    // we need to check again mRingOrCallActive, not enteringRingOrCall
+                    if (mRingOrCallActive) {
+                        mFocusEnforcer.mutePlayersForCall(USAGES_TO_MUTE_IN_RING_OR_CALL);
+                    } else {
+                        mFocusEnforcer.unmutePlayersForCall();
+                    }
+                }
+            }
+        }.start();
+    }
+
+    public void updateMultiAudioFocus(boolean enabled) {
+        Log.d(TAG, "updateMultiAudioFocus( " + enabled + " )");
+        mMultiAudioFocusEnabled = enabled;
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.MULTI_AUDIO_FOCUS_ENABLED, enabled ? 1 : 0);
+        if (!mFocusStack.isEmpty()) {
+            final FocusRequester fr = mFocusStack.peek();
+            fr.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null, false);
+        }
+        if (!enabled) {
+            if (!mMultiAudioFocusList.isEmpty()) {
+                for (FocusRequester multifr : mMultiAudioFocusList) {
+                    multifr.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null, false);
+                }
+                mMultiAudioFocusList.clear();
+            }
+        }
+    }
+
+    public boolean getMultiAudioFocusEnabled() {
+        return mMultiAudioFocusEnabled;
+    }
+
+    private void dumpMultiAudioFocus(PrintWriter pw) {
+        pw.println("Multi Audio Focus enabled :" + mMultiAudioFocusEnabled);
+        if (!mMultiAudioFocusList.isEmpty()) {
+            pw.println("Multi Audio Focus List:");
+            pw.println("------------------------------");
+            for (FocusRequester multifr : mMultiAudioFocusList) {
+                multifr.dump(pw);
+            }
+            pw.println("------------------------------");
+        }
+    }
+}
diff --git a/com/android/server/audio/PlaybackActivityMonitor.java b/com/android/server/audio/PlaybackActivityMonitor.java
new file mode 100644
index 0000000..98f409e
--- /dev/null
+++ b/com/android/server/audio/PlaybackActivityMonitor.java
@@ -0,0 +1,949 @@
+/*
+ * Copyright (C) 2016 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 android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioSystem;
+import android.media.IPlaybackConfigDispatcher;
+import android.media.PlayerBase;
+import android.media.VolumeShaper;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Class to receive and dispatch updates from AudioSystem about recording configurations.
+ */
+public final class PlaybackActivityMonitor
+        implements AudioPlaybackConfiguration.PlayerDeathMonitor, PlayerFocusEnforcer {
+
+    public static final String TAG = "AudioService.PlaybackActivityMonitor";
+
+    private static final boolean DEBUG = false;
+    private static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1;
+
+    private static final VolumeShaper.Configuration DUCK_VSHAPE =
+            new VolumeShaper.Configuration.Builder()
+                .setId(VOLUME_SHAPER_SYSTEM_DUCK_ID)
+                .setCurve(new float[] { 0.f, 1.f } /* times */,
+                    new float[] { 1.f, 0.2f } /* volumes */)
+                .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
+                .setDuration(MediaFocusControl.getFocusRampTimeMs(
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+                    new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION)
+                            .build()))
+                .build();
+    private static final VolumeShaper.Configuration DUCK_ID =
+            new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_DUCK_ID);
+    private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
+            new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
+                    .createIfNeeded()
+                    .build();
+
+    // TODO support VolumeShaper on those players
+    private static final int[] UNDUCKABLE_PLAYER_TYPES = {
+            AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO,
+            AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL,
+    };
+
+    // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp
+    private static final VolumeShaper.Operation PLAY_SKIP_RAMP =
+            new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build();
+
+    private final ArrayList<PlayMonitorClient> mClients = new ArrayList<PlayMonitorClient>();
+    // a public client is one that needs an anonymized version of the playback configurations, we
+    // keep track of whether there is at least one to know when we need to create the list of
+    // playback configurations that do not contain uid/pid/package name information.
+    private boolean mHasPublicClients = false;
+
+    private final Object mPlayerLock = new Object();
+    private final HashMap<Integer, AudioPlaybackConfiguration> mPlayers =
+            new HashMap<Integer, AudioPlaybackConfiguration>();
+
+    private final Context mContext;
+    private int mSavedAlarmVolume = -1;
+    private final int mMaxAlarmVolume;
+    private int mPrivilegedAlarmActiveCount = 0;
+
+    PlaybackActivityMonitor(Context context, int maxAlarmVolume) {
+        mContext = context;
+        mMaxAlarmVolume = maxAlarmVolume;
+        PlayMonitorClient.sListenerDeathMonitor = this;
+        AudioPlaybackConfiguration.sPlayerDeathMonitor = this;
+    }
+
+    //=================================================================
+    private final ArrayList<Integer> mBannedUids = new ArrayList<Integer>();
+
+    // see AudioManagerInternal.disableAudioForUid(boolean disable, int uid)
+    public void disableAudioForUid(boolean disable, int uid) {
+        synchronized(mPlayerLock) {
+            final int index = mBannedUids.indexOf(new Integer(uid));
+            if (index >= 0) {
+                if (!disable) {
+                    if (DEBUG) { // hidden behind DEBUG, too noisy otherwise
+                        sEventLogger.log(new AudioEventLogger.StringEvent("unbanning uid:" + uid));
+                    }
+                    mBannedUids.remove(index);
+                    // nothing else to do, future playback requests from this uid are ok
+                } // no else to handle, uid already present, so disabling again is no-op
+            } else {
+                if (disable) {
+                    for (AudioPlaybackConfiguration apc : mPlayers.values()) {
+                        checkBanPlayer(apc, uid);
+                    }
+                    if (DEBUG) { // hidden behind DEBUG, too noisy otherwise
+                        sEventLogger.log(new AudioEventLogger.StringEvent("banning uid:" + uid));
+                    }
+                    mBannedUids.add(new Integer(uid));
+                } // no else to handle, uid already not in list, so enabling again is no-op
+            }
+        }
+    }
+
+    private boolean checkBanPlayer(@NonNull AudioPlaybackConfiguration apc, int uid) {
+        final boolean toBan = (apc.getClientUid() == uid);
+        if (toBan) {
+            final int piid = apc.getPlayerInterfaceId();
+            try {
+                Log.v(TAG, "banning player " + piid + " uid:" + uid);
+                apc.getPlayerProxy().pause();
+            } catch (Exception e) {
+                Log.e(TAG, "error banning player " + piid + " uid:" + uid, e);
+            }
+        }
+        return toBan;
+    }
+
+    //=================================================================
+    // Track players and their states
+    // methods playerAttributes, playerEvent, releasePlayer are all oneway calls
+    //  into AudioService. They trigger synchronous dispatchPlaybackChange() which updates
+    //  all listeners as oneway calls.
+
+    public int trackPlayer(PlayerBase.PlayerIdCard pic) {
+        final int newPiid = AudioSystem.newAudioPlayerId();
+        if (DEBUG) { Log.v(TAG, "trackPlayer() new piid=" + newPiid); }
+        final AudioPlaybackConfiguration apc =
+                new AudioPlaybackConfiguration(pic, newPiid,
+                        Binder.getCallingUid(), Binder.getCallingPid());
+        apc.init();
+        synchronized (mAllowedCapturePolicies) {
+            int uid = apc.getClientUid();
+            if (mAllowedCapturePolicies.containsKey(uid)) {
+                updateAllowedCapturePolicy(apc, mAllowedCapturePolicies.get(uid));
+            }
+        }
+        sEventLogger.log(new NewPlayerEvent(apc));
+        synchronized(mPlayerLock) {
+            mPlayers.put(newPiid, apc);
+        }
+        return newPiid;
+    }
+
+    public void playerAttributes(int piid, @NonNull AudioAttributes attr, int binderUid) {
+        final boolean change;
+        synchronized (mAllowedCapturePolicies) {
+            if (mAllowedCapturePolicies.containsKey(binderUid)
+                    && attr.getAllowedCapturePolicy() < mAllowedCapturePolicies.get(binderUid)) {
+                attr = new AudioAttributes.Builder(attr)
+                        .setAllowedCapturePolicy(mAllowedCapturePolicies.get(binderUid)).build();
+            }
+        }
+        synchronized(mPlayerLock) {
+            final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
+            if (checkConfigurationCaller(piid, apc, binderUid)) {
+                sEventLogger.log(new AudioAttrEvent(piid, attr));
+                change = apc.handleAudioAttributesEvent(attr);
+            } else {
+                Log.e(TAG, "Error updating audio attributes");
+                change = false;
+            }
+        }
+        if (change) {
+            dispatchPlaybackChange(false);
+        }
+    }
+
+    private static final int FLAGS_FOR_SILENCE_OVERRIDE =
+            AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY |
+            AudioAttributes.FLAG_BYPASS_MUTE;
+
+    private void checkVolumeForPrivilegedAlarm(AudioPlaybackConfiguration apc, int event) {
+        if (event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED ||
+                apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+            if ((apc.getAudioAttributes().getAllFlags() & FLAGS_FOR_SILENCE_OVERRIDE)
+                        == FLAGS_FOR_SILENCE_OVERRIDE  &&
+                    apc.getAudioAttributes().getUsage() == AudioAttributes.USAGE_ALARM &&
+                    mContext.checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE,
+                            apc.getClientPid(), apc.getClientUid()) ==
+                            PackageManager.PERMISSION_GRANTED) {
+                if (event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED &&
+                        apc.getPlayerState() != AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                    if (mPrivilegedAlarmActiveCount++ == 0) {
+                        mSavedAlarmVolume = AudioSystem.getStreamVolumeIndex(
+                                AudioSystem.STREAM_ALARM, AudioSystem.DEVICE_OUT_SPEAKER);
+                        AudioSystem.setStreamVolumeIndexAS(AudioSystem.STREAM_ALARM,
+                                mMaxAlarmVolume, AudioSystem.DEVICE_OUT_SPEAKER);
+                    }
+                } else if (event != AudioPlaybackConfiguration.PLAYER_STATE_STARTED &&
+                        apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                    if (--mPrivilegedAlarmActiveCount == 0) {
+                        if (AudioSystem.getStreamVolumeIndex(
+                                AudioSystem.STREAM_ALARM, AudioSystem.DEVICE_OUT_SPEAKER) ==
+                                mMaxAlarmVolume) {
+                            AudioSystem.setStreamVolumeIndexAS(AudioSystem.STREAM_ALARM,
+                                    mSavedAlarmVolume, AudioSystem.DEVICE_OUT_SPEAKER);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public void playerEvent(int piid, int event, int binderUid) {
+        if (DEBUG) { Log.v(TAG, String.format("playerEvent(piid=%d, event=%d)", piid, event)); }
+        final boolean change;
+        synchronized(mPlayerLock) {
+            final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
+            if (apc == null) {
+                return;
+            }
+            sEventLogger.log(new PlayerEvent(piid, event));
+            if (event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                for (Integer uidInteger: mBannedUids) {
+                    if (checkBanPlayer(apc, uidInteger.intValue())) {
+                        // player was banned, do not update its state
+                        sEventLogger.log(new AudioEventLogger.StringEvent(
+                                "not starting piid:" + piid + " ,is banned"));
+                        return;
+                    }
+                }
+            }
+            if (apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+                // FIXME SoundPool not ready for state reporting
+                return;
+            }
+            if (checkConfigurationCaller(piid, apc, binderUid)) {
+                //TODO add generation counter to only update to the latest state
+                checkVolumeForPrivilegedAlarm(apc, event);
+                change = apc.handleStateEvent(event);
+            } else {
+                Log.e(TAG, "Error handling event " + event);
+                change = false;
+            }
+            if (change && event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                mDuckingManager.checkDuck(apc);
+            }
+        }
+        if (change) {
+            dispatchPlaybackChange(event == AudioPlaybackConfiguration.PLAYER_STATE_RELEASED);
+        }
+    }
+
+    public void playerHasOpPlayAudio(int piid, boolean hasOpPlayAudio, int binderUid) {
+        // no check on UID yet because this is only for logging at the moment
+        sEventLogger.log(new PlayerOpPlayAudioEvent(piid, hasOpPlayAudio, binderUid));
+    }
+
+    public void releasePlayer(int piid, int binderUid) {
+        if (DEBUG) { Log.v(TAG, "releasePlayer() for piid=" + piid); }
+        boolean change = false;
+        synchronized(mPlayerLock) {
+            final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
+            if (checkConfigurationCaller(piid, apc, binderUid)) {
+                sEventLogger.log(new AudioEventLogger.StringEvent(
+                        "releasing player piid:" + piid));
+                mPlayers.remove(new Integer(piid));
+                mDuckingManager.removeReleased(apc);
+                checkVolumeForPrivilegedAlarm(apc, AudioPlaybackConfiguration.PLAYER_STATE_RELEASED);
+                change = apc.handleStateEvent(AudioPlaybackConfiguration.PLAYER_STATE_RELEASED);
+            }
+        }
+        if (change) {
+            dispatchPlaybackChange(true /*iplayerreleased*/);
+        }
+    }
+
+    /**
+     * A map of uid to capture policy.
+     */
+    private final HashMap<Integer, Integer> mAllowedCapturePolicies =
+            new HashMap<Integer, Integer>();
+
+    /**
+     * Cache allowed capture policy, which specifies whether the audio played by the app may or may
+     * not be captured by other apps or the system.
+     *
+     * @param uid the uid of requested app
+     * @param capturePolicy one of
+     *     {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL},
+     *     {@link AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM},
+     *     {@link AudioAttributes#ALLOW_CAPTURE_BY_NONE}.
+     */
+    public void setAllowedCapturePolicy(int uid, int capturePolicy) {
+        synchronized (mAllowedCapturePolicies) {
+            if (capturePolicy == AudioAttributes.ALLOW_CAPTURE_BY_ALL) {
+                // When the capture policy is ALLOW_CAPTURE_BY_ALL, it is okay to
+                // remove it from cached capture policy as it is the default value.
+                mAllowedCapturePolicies.remove(uid);
+                return;
+            } else {
+                mAllowedCapturePolicies.put(uid, capturePolicy);
+            }
+        }
+        synchronized (mPlayerLock) {
+            for (AudioPlaybackConfiguration apc : mPlayers.values()) {
+                if (apc.getClientUid() == uid) {
+                    updateAllowedCapturePolicy(apc, capturePolicy);
+                }
+            }
+        }
+    }
+
+    /**
+     * Return the capture policy for given uid.
+     * @param uid the uid to query its cached capture policy.
+     * @return cached capture policy for given uid or AudioAttributes.ALLOW_CAPTURE_BY_ALL
+     *         if there is not cached capture policy.
+     */
+    public int getAllowedCapturePolicy(int uid) {
+        return mAllowedCapturePolicies.getOrDefault(uid, AudioAttributes.ALLOW_CAPTURE_BY_ALL);
+    }
+
+    /**
+     * Return all cached capture policies.
+     */
+    public HashMap<Integer, Integer> getAllAllowedCapturePolicies() {
+        return mAllowedCapturePolicies;
+    }
+
+    private void updateAllowedCapturePolicy(AudioPlaybackConfiguration apc, int capturePolicy) {
+        AudioAttributes attr = apc.getAudioAttributes();
+        if (attr.getAllowedCapturePolicy() >= capturePolicy) {
+            return;
+        }
+        apc.handleAudioAttributesEvent(
+                new AudioAttributes.Builder(apc.getAudioAttributes())
+                        .setAllowedCapturePolicy(capturePolicy).build());
+    }
+
+    // Implementation of AudioPlaybackConfiguration.PlayerDeathMonitor
+    @Override
+    public void playerDeath(int piid) {
+        releasePlayer(piid, 0);
+    }
+
+    protected void dump(PrintWriter pw) {
+        // players
+        pw.println("\nPlaybackActivityMonitor dump time: "
+                + DateFormat.getTimeInstance().format(new Date()));
+        synchronized(mPlayerLock) {
+            pw.println("\n  playback listeners:");
+            synchronized(mClients) {
+                for (PlayMonitorClient pmc : mClients) {
+                    pw.print(" " + (pmc.mIsPrivileged ? "(S)" : "(P)")
+                            + pmc.toString());
+                }
+            }
+            pw.println("\n");
+            // all players
+            pw.println("\n  players:");
+            final List<Integer> piidIntList = new ArrayList<Integer>(mPlayers.keySet());
+            Collections.sort(piidIntList);
+            for (Integer piidInt : piidIntList) {
+                final AudioPlaybackConfiguration apc = mPlayers.get(piidInt);
+                if (apc != null) {
+                    apc.dump(pw);
+                }
+            }
+            // ducked players
+            pw.println("\n  ducked players piids:");
+            mDuckingManager.dump(pw);
+            // players muted due to the device ringing or being in a call
+            pw.print("\n  muted player piids:");
+            for (int piid : mMutedPlayers) {
+                pw.print(" " + piid);
+            }
+            pw.println();
+            // banned players:
+            pw.print("\n  banned uids:");
+            for (int uid : mBannedUids) {
+                pw.print(" " + uid);
+            }
+            pw.println("\n");
+            // log
+            sEventLogger.dump(pw);
+        }
+        synchronized (mAllowedCapturePolicies) {
+            pw.println("\n  allowed capture policies:");
+            for (HashMap.Entry<Integer, Integer> entry : mAllowedCapturePolicies.entrySet()) {
+                pw.println("  uid: " + entry.getKey() + " policy: " + entry.getValue());
+            }
+        }
+    }
+
+    /**
+     * Check that piid and uid are valid for the given valid configuration.
+     * @param piid the piid of the player.
+     * @param apc the configuration found for this piid.
+     * @param binderUid actual uid of client trying to signal a player state/event/attributes.
+     * @return true if the call is valid and the change should proceed, false otherwise. Always
+     *      returns false when apc is null.
+     */
+    private static boolean checkConfigurationCaller(int piid,
+            final AudioPlaybackConfiguration apc, int binderUid) {
+        if (apc == null) {
+            return false;
+        } else if ((binderUid != 0) && (apc.getClientUid() != binderUid)) {
+            Log.e(TAG, "Forbidden operation from uid " + binderUid + " for player " + piid);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Sends new list after update of playback configurations
+     * @param iplayerReleased indicates if the change was due to a player being released
+     */
+    private void dispatchPlaybackChange(boolean iplayerReleased) {
+        synchronized (mClients) {
+            // typical use case, nobody is listening, don't do any work
+            if (mClients.isEmpty()) {
+                return;
+            }
+        }
+        if (DEBUG) { Log.v(TAG, "dispatchPlaybackChange to " + mClients.size() + " clients"); }
+        final List<AudioPlaybackConfiguration> configsSystem;
+        // list of playback configurations for "public consumption". It is only computed if there
+        // are non-system playback activity listeners.
+        final List<AudioPlaybackConfiguration> configsPublic;
+        synchronized (mPlayerLock) {
+            if (mPlayers.isEmpty()) {
+                return;
+            }
+            configsSystem = new ArrayList<AudioPlaybackConfiguration>(mPlayers.values());
+        }
+        synchronized (mClients) {
+            // was done at beginning of method, but could have changed
+            if (mClients.isEmpty()) {
+                return;
+            }
+            configsPublic = mHasPublicClients ? anonymizeForPublicConsumption(configsSystem) : null;
+            final Iterator<PlayMonitorClient> clientIterator = mClients.iterator();
+            while (clientIterator.hasNext()) {
+                final PlayMonitorClient pmc = clientIterator.next();
+                try {
+                    // do not spam the logs if there are problems communicating with this client
+                    if (pmc.mErrorCount < PlayMonitorClient.MAX_ERRORS) {
+                        if (pmc.mIsPrivileged) {
+                            pmc.mDispatcherCb.dispatchPlaybackConfigChange(configsSystem,
+                                    iplayerReleased);
+                        } else {
+                            // non-system clients don't have the control interface IPlayer, so
+                            // they don't need to flush commands when a player was released
+                            pmc.mDispatcherCb.dispatchPlaybackConfigChange(configsPublic, false);
+                        }
+                    }
+                } catch (RemoteException e) {
+                    pmc.mErrorCount++;
+                    Log.e(TAG, "Error (" + pmc.mErrorCount +
+                            ") trying to dispatch playback config change to " + pmc, e);
+                }
+            }
+        }
+    }
+
+    private ArrayList<AudioPlaybackConfiguration> anonymizeForPublicConsumption(
+            List<AudioPlaybackConfiguration> sysConfigs) {
+        ArrayList<AudioPlaybackConfiguration> publicConfigs =
+                new ArrayList<AudioPlaybackConfiguration>();
+        // only add active anonymized configurations,
+        for (AudioPlaybackConfiguration config : sysConfigs) {
+            if (config.isActive()) {
+                publicConfigs.add(AudioPlaybackConfiguration.anonymizedCopy(config));
+            }
+        }
+        return publicConfigs;
+    }
+
+
+    //=================================================================
+    // PlayerFocusEnforcer implementation
+    private final ArrayList<Integer> mMutedPlayers = new ArrayList<Integer>();
+
+    private final DuckingManager mDuckingManager = new DuckingManager();
+
+    @Override
+    public boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser,
+                               boolean forceDuck) {
+        if (DEBUG) {
+            Log.v(TAG, String.format("duckPlayers: uids winner=%d loser=%d",
+                    winner.getClientUid(), loser.getClientUid()));
+        }
+        synchronized (mPlayerLock) {
+            if (mPlayers.isEmpty()) {
+                return true;
+            }
+            // check if this UID needs to be ducked (return false if not), and gather list of
+            // eligible players to duck
+            final Iterator<AudioPlaybackConfiguration> apcIterator = mPlayers.values().iterator();
+            final ArrayList<AudioPlaybackConfiguration> apcsToDuck =
+                    new ArrayList<AudioPlaybackConfiguration>();
+            while (apcIterator.hasNext()) {
+                final AudioPlaybackConfiguration apc = apcIterator.next();
+                if (!winner.hasSameUid(apc.getClientUid())
+                        && loser.hasSameUid(apc.getClientUid())
+                        && apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED)
+                {
+                    if (!forceDuck && (apc.getAudioAttributes().getContentType() ==
+                            AudioAttributes.CONTENT_TYPE_SPEECH)) {
+                        // the player is speaking, ducking will make the speech unintelligible
+                        // so let the app handle it instead
+                        Log.v(TAG, "not ducking player " + apc.getPlayerInterfaceId()
+                                + " uid:" + apc.getClientUid() + " pid:" + apc.getClientPid()
+                                + " - SPEECH");
+                        return false;
+                    } else if (ArrayUtils.contains(UNDUCKABLE_PLAYER_TYPES, apc.getPlayerType())) {
+                        Log.v(TAG, "not ducking player " + apc.getPlayerInterfaceId()
+                                + " uid:" + apc.getClientUid() + " pid:" + apc.getClientPid()
+                                + " due to type:"
+                                + AudioPlaybackConfiguration.toLogFriendlyPlayerType(
+                                        apc.getPlayerType()));
+                        return false;
+                    }
+                    apcsToDuck.add(apc);
+                }
+            }
+            // add the players eligible for ducking to the list, and duck them
+            // (if apcsToDuck is empty, this will at least mark this uid as ducked, so when
+            //  players of the same uid start, they will be ducked by DuckingManager.checkDuck())
+            mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck);
+        }
+        return true;
+    }
+
+    @Override
+    public void unduckPlayers(@NonNull FocusRequester winner) {
+        if (DEBUG) { Log.v(TAG, "unduckPlayers: uids winner=" + winner.getClientUid()); }
+        synchronized (mPlayerLock) {
+            mDuckingManager.unduckUid(winner.getClientUid(), mPlayers);
+        }
+    }
+
+    @Override
+    public void mutePlayersForCall(int[] usagesToMute) {
+        if (DEBUG) {
+            String log = new String("mutePlayersForCall: usages=");
+            for (int usage : usagesToMute) { log += " " + usage; }
+            Log.v(TAG, log);
+        }
+        synchronized (mPlayerLock) {
+            final Set<Integer> piidSet = mPlayers.keySet();
+            final Iterator<Integer> piidIterator = piidSet.iterator();
+            // find which players to mute
+            while (piidIterator.hasNext()) {
+                final Integer piid = piidIterator.next();
+                final AudioPlaybackConfiguration apc = mPlayers.get(piid);
+                if (apc == null) {
+                    continue;
+                }
+                final int playerUsage = apc.getAudioAttributes().getUsage();
+                boolean mute = false;
+                for (int usageToMute : usagesToMute) {
+                    if (playerUsage == usageToMute) {
+                        mute = true;
+                        break;
+                    }
+                }
+                if (mute) {
+                    try {
+                        sEventLogger.log((new AudioEventLogger.StringEvent("call: muting piid:"
+                                + piid + " uid:" + apc.getClientUid())).printLog(TAG));
+                        apc.getPlayerProxy().setVolume(0.0f);
+                        mMutedPlayers.add(new Integer(piid));
+                    } catch (Exception e) {
+                        Log.e(TAG, "call: error muting player " + piid, e);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void unmutePlayersForCall() {
+        if (DEBUG) {
+            Log.v(TAG, "unmutePlayersForCall()");
+        }
+        synchronized (mPlayerLock) {
+            if (mMutedPlayers.isEmpty()) {
+                return;
+            }
+            for (int piid : mMutedPlayers) {
+                final AudioPlaybackConfiguration apc = mPlayers.get(piid);
+                if (apc != null) {
+                    try {
+                        sEventLogger.log(new AudioEventLogger.StringEvent("call: unmuting piid:"
+                                + piid).printLog(TAG));
+                        apc.getPlayerProxy().setVolume(1.0f);
+                    } catch (Exception e) {
+                        Log.e(TAG, "call: error unmuting player " + piid + " uid:"
+                                + apc.getClientUid(), e);
+                    }
+                }
+            }
+            mMutedPlayers.clear();
+        }
+    }
+
+    //=================================================================
+    // Track playback activity listeners
+
+    void registerPlaybackCallback(IPlaybackConfigDispatcher pcdb, boolean isPrivileged) {
+        if (pcdb == null) {
+            return;
+        }
+        synchronized(mClients) {
+            final PlayMonitorClient pmc = new PlayMonitorClient(pcdb, isPrivileged);
+            if (pmc.init()) {
+                if (!isPrivileged) {
+                    mHasPublicClients = true;
+                }
+                mClients.add(pmc);
+            }
+        }
+    }
+
+    void unregisterPlaybackCallback(IPlaybackConfigDispatcher pcdb) {
+        if (pcdb == null) {
+            return;
+        }
+        synchronized(mClients) {
+            final Iterator<PlayMonitorClient> clientIterator = mClients.iterator();
+            boolean hasPublicClients = false;
+            // iterate over the clients to remove the dispatcher to remove, and reevaluate at
+            // the same time if we still have a public client.
+            while (clientIterator.hasNext()) {
+                PlayMonitorClient pmc = clientIterator.next();
+                if (pcdb.equals(pmc.mDispatcherCb)) {
+                    pmc.release();
+                    clientIterator.remove();
+                } else {
+                    if (!pmc.mIsPrivileged) {
+                        hasPublicClients = true;
+                    }
+                }
+            }
+            mHasPublicClients = hasPublicClients;
+        }
+    }
+
+    List<AudioPlaybackConfiguration> getActivePlaybackConfigurations(boolean isPrivileged) {
+        synchronized(mPlayers) {
+            if (isPrivileged) {
+                return new ArrayList<AudioPlaybackConfiguration>(mPlayers.values());
+            } else {
+                final List<AudioPlaybackConfiguration> configsPublic;
+                synchronized (mPlayerLock) {
+                    configsPublic = anonymizeForPublicConsumption(
+                            new ArrayList<AudioPlaybackConfiguration>(mPlayers.values()));
+                }
+                return configsPublic;
+            }
+        }
+    }
+
+
+    /**
+     * Inner class to track clients that want to be notified of playback updates
+     */
+    private static final class PlayMonitorClient implements IBinder.DeathRecipient {
+
+        // can afford to be static because only one PlaybackActivityMonitor ever instantiated
+        static PlaybackActivityMonitor sListenerDeathMonitor;
+
+        final IPlaybackConfigDispatcher mDispatcherCb;
+        final boolean mIsPrivileged;
+
+        int mErrorCount = 0;
+        // number of errors after which we don't update this client anymore to not spam the logs
+        static final int MAX_ERRORS = 5;
+
+        PlayMonitorClient(IPlaybackConfigDispatcher pcdb, boolean isPrivileged) {
+            mDispatcherCb = pcdb;
+            mIsPrivileged = isPrivileged;
+        }
+
+        public void binderDied() {
+            Log.w(TAG, "client died");
+            sListenerDeathMonitor.unregisterPlaybackCallback(mDispatcherCb);
+        }
+
+        boolean init() {
+            try {
+                mDispatcherCb.asBinder().linkToDeath(this, 0);
+                return true;
+            } catch (RemoteException e) {
+                Log.w(TAG, "Could not link to client death", e);
+                return false;
+            }
+        }
+
+        void release() {
+            mDispatcherCb.asBinder().unlinkToDeath(this, 0);
+        }
+    }
+
+    //=================================================================
+    // Class to handle ducking related operations for a given UID
+    private static final class DuckingManager {
+        private final HashMap<Integer, DuckedApp> mDuckers = new HashMap<Integer, DuckedApp>();
+
+        synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck) {
+            if (DEBUG) {  Log.v(TAG, "DuckingManager: duckUid() uid:"+ uid); }
+            if (!mDuckers.containsKey(uid)) {
+                mDuckers.put(uid, new DuckedApp(uid));
+            }
+            final DuckedApp da = mDuckers.get(uid);
+            for (AudioPlaybackConfiguration apc : apcsToDuck) {
+                da.addDuck(apc, false /*skipRamp*/);
+            }
+        }
+
+        synchronized void unduckUid(int uid, HashMap<Integer, AudioPlaybackConfiguration> players) {
+            if (DEBUG) {  Log.v(TAG, "DuckingManager: unduckUid() uid:"+ uid); }
+            final DuckedApp da = mDuckers.remove(uid);
+            if (da == null) {
+                return;
+            }
+            da.removeUnduckAll(players);
+        }
+
+        // pre-condition: apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
+        synchronized void checkDuck(@NonNull AudioPlaybackConfiguration apc) {
+            if (DEBUG) {  Log.v(TAG, "DuckingManager: checkDuck() player piid:"
+                    + apc.getPlayerInterfaceId()+ " uid:"+ apc.getClientUid()); }
+            final DuckedApp da = mDuckers.get(apc.getClientUid());
+            if (da == null) {
+                return;
+            }
+            da.addDuck(apc, true /*skipRamp*/);
+        }
+
+        synchronized void dump(PrintWriter pw) {
+            for (DuckedApp da : mDuckers.values()) {
+                da.dump(pw);
+            }
+        }
+
+        synchronized void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
+            final int uid = apc.getClientUid();
+            if (DEBUG) {  Log.v(TAG, "DuckingManager: removedReleased() player piid: "
+                    + apc.getPlayerInterfaceId() + " uid:" + uid); }
+            final DuckedApp da = mDuckers.get(uid);
+            if (da == null) {
+                return;
+            }
+            da.removeReleased(apc);
+        }
+
+        private static final class DuckedApp {
+            private final int mUid;
+            private final ArrayList<Integer> mDuckedPlayers = new ArrayList<Integer>();
+
+            DuckedApp(int uid) {
+                mUid = uid;
+            }
+
+            void dump(PrintWriter pw) {
+                pw.print("\t uid:" + mUid + " piids:");
+                for (int piid : mDuckedPlayers) {
+                    pw.print(" " + piid);
+                }
+                pw.println("");
+            }
+
+            // pre-conditions:
+            //  * apc != null
+            //  * apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
+            void addDuck(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
+                final int piid = new Integer(apc.getPlayerInterfaceId());
+                if (mDuckedPlayers.contains(piid)) {
+                    if (DEBUG) { Log.v(TAG, "player piid:" + piid + " already ducked"); }
+                    return;
+                }
+                try {
+                    sEventLogger.log((new DuckEvent(apc, skipRamp)).printLog(TAG));
+                    apc.getPlayerProxy().applyVolumeShaper(
+                            DUCK_VSHAPE,
+                            skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
+                    mDuckedPlayers.add(piid);
+                } catch (Exception e) {
+                    Log.e(TAG, "Error ducking player piid:" + piid + " uid:" + mUid, e);
+                }
+            }
+
+            void removeUnduckAll(HashMap<Integer, AudioPlaybackConfiguration> players) {
+                for (int piid : mDuckedPlayers) {
+                    final AudioPlaybackConfiguration apc = players.get(piid);
+                    if (apc != null) {
+                        try {
+                            sEventLogger.log((new AudioEventLogger.StringEvent("unducking piid:"
+                                    + piid)).printLog(TAG));
+                            apc.getPlayerProxy().applyVolumeShaper(
+                                    DUCK_ID,
+                                    VolumeShaper.Operation.REVERSE);
+                        } catch (Exception e) {
+                            Log.e(TAG, "Error unducking player piid:" + piid + " uid:" + mUid, e);
+                        }
+                    } else {
+                        // this piid was in the list of ducked players, but wasn't found
+                        if (DEBUG) {
+                            Log.v(TAG, "Error unducking player piid:" + piid
+                                    + ", player not found for uid " + mUid);
+                        }
+                    }
+                }
+                mDuckedPlayers.clear();
+            }
+
+            void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
+                mDuckedPlayers.remove(new Integer(apc.getPlayerInterfaceId()));
+            }
+        }
+    }
+
+    //=================================================================
+    // For logging
+    private final static class PlayerEvent extends AudioEventLogger.Event {
+        // only keeping the player interface ID as it uniquely identifies the player in the event
+        final int mPlayerIId;
+        final int mState;
+
+        PlayerEvent(int piid, int state) {
+            mPlayerIId = piid;
+            mState = state;
+        }
+
+        @Override
+        public String eventToString() {
+            return new StringBuilder("player piid:").append(mPlayerIId).append(" state:")
+                    .append(AudioPlaybackConfiguration.toLogFriendlyPlayerState(mState)).toString();
+        }
+    }
+
+    private final static class PlayerOpPlayAudioEvent extends AudioEventLogger.Event {
+        // only keeping the player interface ID as it uniquely identifies the player in the event
+        final int mPlayerIId;
+        final boolean mHasOp;
+        final int mUid;
+
+        PlayerOpPlayAudioEvent(int piid, boolean hasOp, int uid) {
+            mPlayerIId = piid;
+            mHasOp = hasOp;
+            mUid = uid;
+        }
+
+        @Override
+        public String eventToString() {
+            return new StringBuilder("player piid:").append(mPlayerIId)
+                    .append(" has OP_PLAY_AUDIO:").append(mHasOp)
+                    .append(" in uid:").append(mUid).toString();
+        }
+    }
+
+    private final static class NewPlayerEvent extends AudioEventLogger.Event {
+        private final int mPlayerIId;
+        private final int mPlayerType;
+        private final int mClientUid;
+        private final int mClientPid;
+        private final AudioAttributes mPlayerAttr;
+
+        NewPlayerEvent(AudioPlaybackConfiguration apc) {
+            mPlayerIId = apc.getPlayerInterfaceId();
+            mPlayerType = apc.getPlayerType();
+            mClientUid = apc.getClientUid();
+            mClientPid = apc.getClientPid();
+            mPlayerAttr = apc.getAudioAttributes();
+        }
+
+        @Override
+        public String eventToString() {
+            return new String("new player piid:" + mPlayerIId + " uid/pid:" + mClientUid + "/"
+                    + mClientPid + " type:"
+                    + AudioPlaybackConfiguration.toLogFriendlyPlayerType(mPlayerType)
+                    + " attr:" + mPlayerAttr);
+        }
+    }
+
+    private static final class DuckEvent extends AudioEventLogger.Event {
+        private final int mPlayerIId;
+        private final boolean mSkipRamp;
+        private final int mClientUid;
+        private final int mClientPid;
+
+        DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
+            mPlayerIId = apc.getPlayerInterfaceId();
+            mSkipRamp = skipRamp;
+            mClientUid = apc.getClientUid();
+            mClientPid = apc.getClientPid();
+        }
+
+        @Override
+        public String eventToString() {
+            return new StringBuilder("ducking player piid:").append(mPlayerIId)
+                    .append(" uid/pid:").append(mClientUid).append("/").append(mClientPid)
+                    .append(" skip ramp:").append(mSkipRamp).toString();
+        }
+    }
+
+    private static final class AudioAttrEvent extends AudioEventLogger.Event {
+        private final int mPlayerIId;
+        private final AudioAttributes mPlayerAttr;
+
+        AudioAttrEvent(int piid, AudioAttributes attr) {
+            mPlayerIId = piid;
+            mPlayerAttr = attr;
+        }
+
+        @Override
+        public String eventToString() {
+            return new String("player piid:" + mPlayerIId + " new AudioAttributes:" + mPlayerAttr);
+        }
+    }
+
+    private static final AudioEventLogger sEventLogger = new AudioEventLogger(100,
+            "playback activity as reported through PlayerBase");
+}
diff --git a/com/android/server/audio/PlayerFocusEnforcer.java b/com/android/server/audio/PlayerFocusEnforcer.java
new file mode 100644
index 0000000..89e7b78
--- /dev/null
+++ b/com/android/server/audio/PlayerFocusEnforcer.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 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 android.annotation.NonNull;
+
+public interface PlayerFocusEnforcer {
+
+    /**
+     * Ducks the players associated with the "loser" focus owner (i.e. same UID). Returns true if
+     * at least one active player was found and ducked, false otherwise.
+     * @param winner
+     * @param loser
+     * @return
+     */
+    boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser,
+                               boolean forceDuck);
+
+    /**
+     * Unduck the players that had been ducked with
+     * {@link #duckPlayers(FocusRequester, FocusRequester, boolean)}
+     * @param winner
+     */
+    void unduckPlayers(@NonNull FocusRequester winner);
+
+    /**
+     * Mute players at the beginning of a call
+     * @param usagesToMute array of {@link android.media.AudioAttributes} usages to mute
+     */
+    void mutePlayersForCall(int[] usagesToMute);
+
+    /**
+     * Unmute players at the end of a call
+     */
+    void unmutePlayersForCall();
+}
\ No newline at end of file
diff --git a/com/android/server/audio/RecordingActivityMonitor.java b/com/android/server/audio/RecordingActivityMonitor.java
new file mode 100644
index 0000000..32c6cc3
--- /dev/null
+++ b/com/android/server/audio/RecordingActivityMonitor.java
@@ -0,0 +1,623 @@
+/*
+ * Copyright (C) 2016 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 android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecordingConfiguration;
+import android.media.AudioSystem;
+import android.media.IRecordingConfigDispatcher;
+import android.media.MediaRecorder;
+import android.media.audiofx.AudioEffect;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Class to receive and dispatch updates from AudioSystem about recording configurations.
+ */
+public final class RecordingActivityMonitor implements AudioSystem.AudioRecordingCallback {
+
+    public final static String TAG = "AudioService.RecordingActivityMonitor";
+
+    private ArrayList<RecMonitorClient> mClients = new ArrayList<RecMonitorClient>();
+    // a public client is one that needs an anonymized version of the playback configurations, we
+    // keep track of whether there is at least one to know when we need to create the list of
+    // playback configurations that do not contain uid/package name information.
+    private boolean mHasPublicClients = false;
+
+
+    // When legacy remote submix device is active, remote submix device should not be fixed and
+    // full volume device. When legacy remote submix device is active, there will be a recording
+    // activity using device with type as {@link AudioSystem.DEVICE_OUT_REMOTE_SUBMIX} and address
+    // as {@link AudioSystem.LEGACY_REMOTE_SUBMIX_ADDRESS}. Cache riid of legacy remote submix
+    // since remote submix state is not cached in mRecordStates.
+    private AtomicInteger mLegacyRemoteSubmixRiid =
+            new AtomicInteger(AudioManager.RECORD_RIID_INVALID);
+    private AtomicBoolean mLegacyRemoteSubmixActive = new AtomicBoolean(false);
+
+    static final class RecordingState {
+        private final int mRiid;
+        private final RecorderDeathHandler mDeathHandler;
+        private boolean mIsActive;
+        private AudioRecordingConfiguration mConfig;
+
+        RecordingState(int riid, RecorderDeathHandler handler) {
+            mRiid = riid;
+            mDeathHandler = handler;
+        }
+
+        RecordingState(AudioRecordingConfiguration config) {
+            mRiid = AudioManager.RECORD_RIID_INVALID;
+            mDeathHandler = null;
+            mConfig = config;
+        }
+
+        int getRiid() {
+            return mRiid;
+        }
+
+        int getPortId() {
+            return mConfig != null ? mConfig.getClientPortId() : -1;
+        }
+
+        AudioRecordingConfiguration getConfig() {
+            return mConfig;
+        }
+
+        boolean hasDeathHandler() {
+            return mDeathHandler != null;
+        }
+
+        boolean isActiveConfiguration() {
+            return mIsActive && mConfig != null;
+        }
+
+        void release() {
+            if (mDeathHandler != null) {
+                mDeathHandler.release();
+            }
+        }
+
+        // returns true if status of an active recording has changed
+        boolean setActive(boolean active) {
+            if (mIsActive == active) return false;
+            mIsActive = active;
+            return mConfig != null;
+        }
+
+        // returns true if an active recording has been updated
+        boolean setConfig(AudioRecordingConfiguration config) {
+            if (config.equals(mConfig)) return false;
+            mConfig = config;
+            return mIsActive;
+        }
+
+        void dump(PrintWriter pw) {
+            pw.println("riid " + mRiid + "; active? " + mIsActive);
+            if (mConfig != null) {
+                mConfig.dump(pw);
+            } else {
+                pw.println("  no config");
+            }
+        }
+    }
+    private List<RecordingState> mRecordStates = new ArrayList<RecordingState>();
+
+    private final PackageManager mPackMan;
+
+    RecordingActivityMonitor(Context ctxt) {
+        RecMonitorClient.sMonitor = this;
+        RecorderDeathHandler.sMonitor = this;
+        mPackMan = ctxt.getPackageManager();
+    }
+
+    /**
+     * Implementation of android.media.AudioSystem.AudioRecordingCallback
+     */
+    public void onRecordingConfigurationChanged(int event, int riid, int uid, int session,
+                                                int source, int portId, boolean silenced,
+                                                int[] recordingInfo,
+                                                AudioEffect.Descriptor[] clientEffects,
+                                                AudioEffect.Descriptor[] effects,
+                                                int activeSource, String packName) {
+        final AudioRecordingConfiguration config = createRecordingConfiguration(
+                uid, session, source, recordingInfo,
+                portId, silenced, activeSource, clientEffects, effects);
+        if (source == MediaRecorder.AudioSource.REMOTE_SUBMIX
+                && (event == AudioManager.RECORD_CONFIG_EVENT_START
+                        || event == AudioManager.RECORD_CONFIG_EVENT_UPDATE)) {
+            final AudioDeviceInfo device = config.getAudioDevice();
+            if (device != null
+                    && AudioSystem.LEGACY_REMOTE_SUBMIX_ADDRESS.equals(device.getAddress())) {
+                mLegacyRemoteSubmixRiid.set(riid);
+                mLegacyRemoteSubmixActive.set(true);
+            }
+        }
+        if (MediaRecorder.isSystemOnlyAudioSource(source)) {
+            // still want to log event, it just won't appear in recording configurations;
+            sEventLogger.log(new RecordingEvent(event, riid, config).printLog(TAG));
+            return;
+        }
+        dispatchCallbacks(updateSnapshot(event, riid, config));
+    }
+
+    /**
+     * Track a recorder provided by the client
+     */
+    public int trackRecorder(IBinder recorder) {
+        if (recorder == null) {
+            Log.e(TAG, "trackRecorder called with null token");
+            return AudioManager.RECORD_RIID_INVALID;
+        }
+        final int newRiid = AudioSystem.newAudioRecorderId();
+        RecorderDeathHandler handler = new RecorderDeathHandler(newRiid, recorder);
+        if (!handler.init()) {
+            // probably means that the AudioRecord has already died
+            return AudioManager.RECORD_RIID_INVALID;
+        }
+        synchronized (mRecordStates) {
+            mRecordStates.add(new RecordingState(newRiid, handler));
+        }
+        // a newly added record is inactive, no change in active configs is possible.
+        return newRiid;
+    }
+
+    /**
+     * Receive an event from the client about a tracked recorder
+     */
+    public void recorderEvent(int riid, int event) {
+        if (mLegacyRemoteSubmixRiid.get() == riid) {
+            mLegacyRemoteSubmixActive.set(event == AudioManager.RECORDER_STATE_STARTED);
+        }
+        int configEvent = event == AudioManager.RECORDER_STATE_STARTED
+                ? AudioManager.RECORD_CONFIG_EVENT_START :
+                event == AudioManager.RECORDER_STATE_STOPPED
+                ? AudioManager.RECORD_CONFIG_EVENT_STOP : AudioManager.RECORD_CONFIG_EVENT_NONE;
+        if (riid == AudioManager.RECORD_RIID_INVALID
+                || configEvent == AudioManager.RECORD_CONFIG_EVENT_NONE) {
+            sEventLogger.log(new RecordingEvent(event, riid, null).printLog(TAG));
+            return;
+        }
+        dispatchCallbacks(updateSnapshot(configEvent, riid, null));
+    }
+
+    /**
+     * Stop tracking the recorder
+     */
+    public void releaseRecorder(int riid) {
+        dispatchCallbacks(updateSnapshot(AudioManager.RECORD_CONFIG_EVENT_RELEASE, riid, null));
+    }
+
+    private void dispatchCallbacks(List<AudioRecordingConfiguration> configs) {
+        if (configs == null) { // null means "no changes"
+            return;
+        }
+        synchronized (mClients) {
+            // list of recording configurations for "public consumption". It is only computed if
+            // there are non-system recording activity listeners.
+            final List<AudioRecordingConfiguration> configsPublic = mHasPublicClients
+                    ? anonymizeForPublicConsumption(configs) :
+                      new ArrayList<AudioRecordingConfiguration>();
+            for (RecMonitorClient rmc : mClients) {
+                try {
+                    if (rmc.mIsPrivileged) {
+                        rmc.mDispatcherCb.dispatchRecordingConfigChange(configs);
+                    } else {
+                        rmc.mDispatcherCb.dispatchRecordingConfigChange(configsPublic);
+                    }
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Could not call dispatchRecordingConfigChange() on client", e);
+                }
+            }
+        }
+    }
+
+    protected void dump(PrintWriter pw) {
+        // recorders
+        pw.println("\nRecordActivityMonitor dump time: "
+                + DateFormat.getTimeInstance().format(new Date()));
+        synchronized (mRecordStates) {
+            for (RecordingState state : mRecordStates) {
+                state.dump(pw);
+            }
+        }
+        pw.println("\n");
+        // log
+        sEventLogger.dump(pw);
+    }
+
+    private static ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption(
+            List<AudioRecordingConfiguration> sysConfigs) {
+        ArrayList<AudioRecordingConfiguration> publicConfigs =
+                new ArrayList<AudioRecordingConfiguration>();
+        // only add active anonymized configurations,
+        for (AudioRecordingConfiguration config : sysConfigs) {
+            publicConfigs.add(AudioRecordingConfiguration.anonymizedCopy(config));
+        }
+        return publicConfigs;
+    }
+
+    void initMonitor() {
+        AudioSystem.setRecordingCallback(this);
+    }
+
+    void onAudioServerDied() {
+        // Remove all RecordingState entries that do not have a death handler (that means
+        // they are tracked by the Audio Server). If there were active entries among removed,
+        // dispatch active configuration changes.
+        List<AudioRecordingConfiguration> configs = null;
+        synchronized (mRecordStates) {
+            boolean configChanged = false;
+            for (Iterator<RecordingState> it = mRecordStates.iterator(); it.hasNext(); ) {
+                RecordingState state = it.next();
+                if (!state.hasDeathHandler()) {
+                    if (state.isActiveConfiguration()) {
+                        configChanged = true;
+                        sEventLogger.log(new RecordingEvent(
+                                        AudioManager.RECORD_CONFIG_EVENT_RELEASE,
+                                        state.getRiid(), state.getConfig()));
+                    }
+                    it.remove();
+                }
+            }
+            if (configChanged) {
+                configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
+            }
+        }
+        dispatchCallbacks(configs);
+    }
+
+    void registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
+        if (rcdb == null) {
+            return;
+        }
+        synchronized (mClients) {
+            final RecMonitorClient rmc = new RecMonitorClient(rcdb, isPrivileged);
+            if (rmc.init()) {
+                if (!isPrivileged) {
+                    mHasPublicClients = true;
+                }
+                mClients.add(rmc);
+            }
+        }
+    }
+
+    void unregisterRecordingCallback(IRecordingConfigDispatcher rcdb) {
+        if (rcdb == null) {
+            return;
+        }
+        synchronized (mClients) {
+            final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
+            boolean hasPublicClients = false;
+            while (clientIterator.hasNext()) {
+                RecMonitorClient rmc = clientIterator.next();
+                if (rcdb.equals(rmc.mDispatcherCb)) {
+                    rmc.release();
+                    clientIterator.remove();
+                } else {
+                    if (!rmc.mIsPrivileged) {
+                        hasPublicClients = true;
+                    }
+                }
+            }
+            mHasPublicClients = hasPublicClients;
+        }
+    }
+
+    List<AudioRecordingConfiguration> getActiveRecordingConfigurations(boolean isPrivileged) {
+        List<AudioRecordingConfiguration> configs = new ArrayList<AudioRecordingConfiguration>();
+        synchronized (mRecordStates) {
+            for (RecordingState state : mRecordStates) {
+                if (state.isActiveConfiguration()) {
+                    configs.add(state.getConfig());
+                }
+            }
+        }
+        // AudioRecordingConfiguration objects never get updated. If config changes,
+        // the reference to the config is set in RecordingState.
+        if (!isPrivileged) {
+            configs = anonymizeForPublicConsumption(configs);
+        }
+        return configs;
+    }
+
+    /**
+     * Return true if legacy remote submix device is active. Otherwise, return false.
+     */
+    boolean isLegacyRemoteSubmixActive() {
+        return mLegacyRemoteSubmixActive.get();
+    }
+
+    /**
+     * Create a recording configuration from the provided parameters
+     * @param uid
+     * @param session
+     * @param source
+     * @param recordingFormat see
+     *     {@link AudioSystem.AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int,\
+     int, int, boolean, int[], AudioEffect.Descriptor[], AudioEffect.Descriptor[], int, String)}
+     *     for the definition of the contents of the array
+     * @param portId
+     * @param silenced
+     * @param activeSource
+     * @param clientEffects
+     * @param effects
+     * @return null a configuration object.
+     */
+    private AudioRecordingConfiguration createRecordingConfiguration(int uid,
+            int session, int source, int[] recordingInfo, int portId, boolean silenced,
+            int activeSource, AudioEffect.Descriptor[] clientEffects,
+            AudioEffect.Descriptor[] effects) {
+        final AudioFormat clientFormat = new AudioFormat.Builder()
+                .setEncoding(recordingInfo[0])
+                // FIXME this doesn't support index-based masks
+                .setChannelMask(recordingInfo[1])
+                .setSampleRate(recordingInfo[2])
+                .build();
+        final AudioFormat deviceFormat = new AudioFormat.Builder()
+                .setEncoding(recordingInfo[3])
+                // FIXME this doesn't support index-based masks
+                .setChannelMask(recordingInfo[4])
+                .setSampleRate(recordingInfo[5])
+                .build();
+        final int patchHandle = recordingInfo[6];
+        final String[] packages = mPackMan.getPackagesForUid(uid);
+        final String packageName;
+        if (packages != null && packages.length > 0) {
+            packageName = packages[0];
+        } else {
+            packageName = "";
+        }
+        return new AudioRecordingConfiguration(uid, session, source,
+                clientFormat, deviceFormat, patchHandle, packageName,
+                portId, silenced, activeSource, clientEffects, effects);
+    }
+
+    /**
+     * Update the internal "view" of the active recording sessions
+     * @param event RECORD_CONFIG_EVENT_...
+     * @param riid
+     * @param config
+     * @return null if the list of active recording sessions has not been modified, a list
+     *     with the current active configurations otherwise.
+     */
+    private List<AudioRecordingConfiguration> updateSnapshot(
+            int event, int riid, AudioRecordingConfiguration config) {
+        List<AudioRecordingConfiguration> configs = null;
+        synchronized (mRecordStates) {
+            int stateIndex = -1;
+            if (riid != AudioManager.RECORD_RIID_INVALID) {
+                stateIndex = findStateByRiid(riid);
+            } else if (config != null) {
+                stateIndex = findStateByPortId(config.getClientPortId());
+            }
+            if (stateIndex == -1) {
+                if (event == AudioManager.RECORD_CONFIG_EVENT_START && config != null) {
+                    // First time registration for a recorder tracked by AudioServer.
+                    mRecordStates.add(new RecordingState(config));
+                    stateIndex = mRecordStates.size() - 1;
+                } else {
+                    if (config == null) {
+                        // Records tracked by clients must be registered first via trackRecorder.
+                        Log.e(TAG, String.format(
+                                        "Unexpected event %d for riid %d", event, riid));
+                    }
+                    return configs;
+                }
+            }
+            final RecordingState state = mRecordStates.get(stateIndex);
+
+            boolean configChanged;
+            switch (event) {
+                case AudioManager.RECORD_CONFIG_EVENT_START:
+                    configChanged = state.setActive(true);
+                    if (config != null) {
+                        configChanged = state.setConfig(config) || configChanged;
+                    }
+                    break;
+                case AudioManager.RECORD_CONFIG_EVENT_UPDATE:
+                    // For this event config != null
+                    configChanged = state.setConfig(config);
+                    break;
+                case AudioManager.RECORD_CONFIG_EVENT_STOP:
+                    configChanged = state.setActive(false);
+                    if (!state.hasDeathHandler()) {
+                        // A recorder tracked by AudioServer has to be removed now so it
+                        // does not leak. It will be re-registered if recording starts again.
+                        mRecordStates.remove(stateIndex);
+                    }
+                    break;
+                case AudioManager.RECORD_CONFIG_EVENT_RELEASE:
+                    configChanged = state.isActiveConfiguration();
+                    state.release();
+                    mRecordStates.remove(stateIndex);
+                    break;
+                default:
+                    Log.e(TAG, String.format("Unknown event %d for riid %d / portid %d",
+                                    event, riid, state.getPortId()));
+                    configChanged = false;
+            }
+            if (configChanged) {
+                sEventLogger.log(new RecordingEvent(event, riid, state.getConfig()));
+                configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
+            }
+        }
+        return configs;
+    }
+
+    // riid is assumed to be valid
+    private int findStateByRiid(int riid) {
+        synchronized (mRecordStates) {
+            for (int i = 0; i < mRecordStates.size(); i++) {
+                if (mRecordStates.get(i).getRiid() == riid) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    private int findStateByPortId(int portId) {
+        // Lookup by portId is unambiguous only for recordings managed by the Audio Server.
+        synchronized (mRecordStates) {
+            for (int i = 0; i < mRecordStates.size(); i++) {
+                if (!mRecordStates.get(i).hasDeathHandler()
+                        && mRecordStates.get(i).getPortId() == portId) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Inner class to track clients that want to be notified of recording updates
+     */
+    private final static class RecMonitorClient implements IBinder.DeathRecipient {
+
+        // can afford to be static because only one RecordingActivityMonitor ever instantiated
+        static RecordingActivityMonitor sMonitor;
+
+        final IRecordingConfigDispatcher mDispatcherCb;
+        final boolean mIsPrivileged;
+
+        RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
+            mDispatcherCb = rcdb;
+            mIsPrivileged = isPrivileged;
+        }
+
+        public void binderDied() {
+            Log.w(TAG, "client died");
+            sMonitor.unregisterRecordingCallback(mDispatcherCb);
+        }
+
+        boolean init() {
+            try {
+                mDispatcherCb.asBinder().linkToDeath(this, 0);
+                return true;
+            } catch (RemoteException e) {
+                Log.w(TAG, "Could not link to client death", e);
+                return false;
+            }
+        }
+
+        void release() {
+            mDispatcherCb.asBinder().unlinkToDeath(this, 0);
+        }
+    }
+
+    private static final class RecorderDeathHandler implements IBinder.DeathRecipient {
+
+        // can afford to be static because only one RecordingActivityMonitor ever instantiated
+        static RecordingActivityMonitor sMonitor;
+
+        final int mRiid;
+        private final IBinder mRecorderToken;
+
+        RecorderDeathHandler(int riid, IBinder recorderToken) {
+            mRiid = riid;
+            mRecorderToken = recorderToken;
+        }
+
+        public void binderDied() {
+            sMonitor.releaseRecorder(mRiid);
+        }
+
+        boolean init() {
+            try {
+                mRecorderToken.linkToDeath(this, 0);
+                return true;
+            } catch (RemoteException e) {
+                Log.w(TAG, "Could not link to recorder death", e);
+                return false;
+            }
+        }
+
+        void release() {
+            mRecorderToken.unlinkToDeath(this, 0);
+        }
+    }
+
+    /**
+     * Inner class for recording event logging
+     */
+    private static final class RecordingEvent extends AudioEventLogger.Event {
+        private final int mRecEvent;
+        private final int mRIId;
+        private final int mClientUid;
+        private final int mSession;
+        private final int mSource;
+        private final String mPackName;
+
+        RecordingEvent(int event, int riid, AudioRecordingConfiguration config) {
+            mRecEvent = event;
+            mRIId = riid;
+            if (config != null) {
+                mClientUid = config.getClientUid();
+                mSession = config.getClientAudioSessionId();
+                mSource = config.getClientAudioSource();
+                mPackName = config.getClientPackageName();
+            } else {
+                mClientUid = -1;
+                mSession = -1;
+                mSource = -1;
+                mPackName = null;
+            }
+        }
+
+        private static String recordEventToString(int recEvent) {
+            switch (recEvent) {
+                case AudioManager.RECORD_CONFIG_EVENT_START:
+                    return "start";
+                case AudioManager.RECORD_CONFIG_EVENT_UPDATE:
+                    return "update";
+                case AudioManager.RECORD_CONFIG_EVENT_STOP:
+                    return "stop";
+                case AudioManager.RECORD_CONFIG_EVENT_RELEASE:
+                    return "release";
+                default:
+                    return "unknown (" + recEvent + ")";
+            }
+        }
+
+        @Override
+        public String eventToString() {
+            return new StringBuilder("rec ").append(recordEventToString(mRecEvent))
+                    .append(" riid:").append(mRIId)
+                    .append(" uid:").append(mClientUid)
+                    .append(" session:").append(mSession)
+                    .append(" src:").append(MediaRecorder.toLogFriendlyAudioSource(mSource))
+                    .append(mPackName == null ? "" : " pack:" + mPackName).toString();
+        }
+    }
+
+    private static final AudioEventLogger sEventLogger = new AudioEventLogger(50,
+            "recording activity received by AudioService");
+}
diff --git a/com/android/server/audio/RotationHelper.java b/com/android/server/audio/RotationHelper.java
new file mode 100644
index 0000000..ad72166
--- /dev/null
+++ b/com/android/server/audio/RotationHelper.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2015 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 android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.media.AudioSystem;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Surface;
+import android.view.WindowManager;
+
+/**
+ * Class to handle device rotation events for AudioService, and forward device rotation
+ * to the audio HALs through AudioSystem.
+ *
+ * The role of this class is to monitor device orientation changes, and upon rotation,
+ * verify the UI orientation. In case of a change, send the new orientation, in increments
+ * of 90deg, through AudioSystem.
+ *
+ * Note that even though we're responding to device orientation events, we always
+ * query the display rotation so audio stays in sync with video/dialogs. This is
+ * done with .getDefaultDisplay().getRotation() from WINDOW_SERVICE.
+ */
+class RotationHelper {
+
+    private static final String TAG = "AudioService.RotationHelper";
+
+    private static AudioDisplayListener sDisplayListener;
+
+    private static final Object sRotationLock = new Object();
+    private static int sDeviceRotation = Surface.ROTATION_0; // R/W synchronized on sRotationLock
+
+    private static Context sContext;
+    private static Handler sHandler;
+
+    /**
+     * post conditions:
+     * - sDisplayListener != null
+     * - sContext != null
+     */
+    static void init(Context context, Handler handler) {
+        if (context == null) {
+            throw new IllegalArgumentException("Invalid null context");
+        }
+        sContext = context;
+        sHandler = handler;
+        sDisplayListener = new AudioDisplayListener();
+        enable();
+    }
+
+    static void enable() {
+        ((DisplayManager) sContext.getSystemService(Context.DISPLAY_SERVICE))
+                .registerDisplayListener(sDisplayListener, sHandler);
+        updateOrientation();
+    }
+
+    static void disable() {
+        ((DisplayManager) sContext.getSystemService(Context.DISPLAY_SERVICE))
+                .unregisterDisplayListener(sDisplayListener);
+    }
+
+    /**
+     * Query current display rotation and publish the change if any.
+     */
+    static void updateOrientation() {
+        // Even though we're responding to device orientation events,
+        // use display rotation so audio stays in sync with video/dialogs
+        // TODO(b/148458001): Support multi-display
+        int newRotation = ((WindowManager) sContext.getSystemService(
+                Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
+        synchronized(sRotationLock) {
+            if (newRotation != sDeviceRotation) {
+                sDeviceRotation = newRotation;
+                publishRotation(sDeviceRotation);
+            }
+        }
+    }
+
+    private static void publishRotation(int rotation) {
+        Log.v(TAG, "publishing device rotation =" + rotation + " (x90deg)");
+        switch (rotation) {
+            case Surface.ROTATION_0:
+                AudioSystem.setParameters("rotation=0");
+                break;
+            case Surface.ROTATION_90:
+                AudioSystem.setParameters("rotation=90");
+                break;
+            case Surface.ROTATION_180:
+                AudioSystem.setParameters("rotation=180");
+                break;
+            case Surface.ROTATION_270:
+                AudioSystem.setParameters("rotation=270");
+                break;
+            default:
+                Log.e(TAG, "Unknown device rotation");
+        }
+    }
+
+    /**
+     * Uses android.hardware.display.DisplayManager.DisplayListener
+     */
+    final static class AudioDisplayListener implements DisplayManager.DisplayListener {
+
+        @Override
+        public void onDisplayAdded(int displayId) {
+        }
+
+        @Override
+        public void onDisplayRemoved(int displayId) {
+        }
+
+        @Override
+        public void onDisplayChanged(int displayId) {
+            updateOrientation();
+        }
+    }
+}
\ No newline at end of file
diff --git a/com/android/server/audio/SoundEffectsHelper.java b/com/android/server/audio/SoundEffectsHelper.java
new file mode 100644
index 0000000..cf5bc8d
--- /dev/null
+++ b/com/android/server/audio/SoundEffectsHelper.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 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 android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.SoundPool;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.PrintWriterPrinter;
+
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A helper class for managing sound effects loading / unloading
+ * used by AudioService. As its methods are called on the message handler thread
+ * of AudioService, the actual work is offloaded to a dedicated thread.
+ * This helps keeping AudioService responsive.
+ * @hide
+ */
+class SoundEffectsHelper {
+    private static final String TAG = "AS.SfxHelper";
+
+    private static final int NUM_SOUNDPOOL_CHANNELS = 4;
+
+    /* Sound effect file names  */
+    private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/";
+
+    private static final int EFFECT_NOT_IN_SOUND_POOL = 0; // SoundPool sample IDs > 0
+
+    private static final int MSG_LOAD_EFFECTS = 0;
+    private static final int MSG_UNLOAD_EFFECTS = 1;
+    private static final int MSG_PLAY_EFFECT = 2;
+    private static final int MSG_LOAD_EFFECTS_TIMEOUT = 3;
+
+    interface OnEffectsLoadCompleteHandler {
+        void run(boolean success);
+    }
+
+    private final AudioEventLogger mSfxLogger = new AudioEventLogger(
+            AudioManager.NUM_SOUND_EFFECTS + 10, "Sound Effects Loading");
+
+    private final Context mContext;
+    // default attenuation applied to sound played with playSoundEffect()
+    private final int mSfxAttenuationDb;
+
+    // thread for doing all work
+    private SfxWorker mSfxWorker;
+    // thread's message handler
+    private SfxHandler mSfxHandler;
+
+    private static final class Resource {
+        final String mFileName;
+        int mSampleId;
+        boolean mLoaded;  // for effects in SoundPool
+        Resource(String fileName) {
+            mFileName = fileName;
+            mSampleId = EFFECT_NOT_IN_SOUND_POOL;
+        }
+    }
+    // All the fields below are accessed by the worker thread exclusively
+    private final List<Resource> mResources = new ArrayList<Resource>();
+    private final int[] mEffects = new int[AudioManager.NUM_SOUND_EFFECTS]; // indexes in mResources
+    private SoundPool mSoundPool;
+    private SoundPoolLoader mSoundPoolLoader;
+
+    SoundEffectsHelper(Context context) {
+        mContext = context;
+        mSfxAttenuationDb = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_soundEffectVolumeDb);
+        startWorker();
+    }
+
+    /*package*/ void loadSoundEffects(OnEffectsLoadCompleteHandler onComplete) {
+        sendMsg(MSG_LOAD_EFFECTS, 0, 0, onComplete, 0);
+    }
+
+    /**
+     *  Unloads samples from the sound pool.
+     *  This method can be called to free some memory when
+     *  sound effects are disabled.
+     */
+    /*package*/ void unloadSoundEffects() {
+        sendMsg(MSG_UNLOAD_EFFECTS, 0, 0, null, 0);
+    }
+
+    /*package*/ void playSoundEffect(int effect, int volume) {
+        sendMsg(MSG_PLAY_EFFECT, effect, volume, null, 0);
+    }
+
+    /*package*/ void dump(PrintWriter pw, String prefix) {
+        if (mSfxHandler != null) {
+            pw.println(prefix + "Message handler (watch for unhandled messages):");
+            mSfxHandler.dump(new PrintWriterPrinter(pw), "  ");
+        } else {
+            pw.println(prefix + "Message handler is null");
+        }
+        pw.println(prefix + "Default attenuation (dB): " + mSfxAttenuationDb);
+        mSfxLogger.dump(pw);
+    }
+
+    private void startWorker() {
+        mSfxWorker = new SfxWorker();
+        mSfxWorker.start();
+        synchronized (this) {
+            while (mSfxHandler == null) {
+                try {
+                    wait();
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Interrupted while waiting " + mSfxWorker.getName() + " to start");
+                }
+            }
+        }
+    }
+
+    private void sendMsg(int msg, int arg1, int arg2, Object obj, int delayMs) {
+        mSfxHandler.sendMessageDelayed(mSfxHandler.obtainMessage(msg, arg1, arg2, obj), delayMs);
+    }
+
+    private void logEvent(String msg) {
+        mSfxLogger.log(new AudioEventLogger.StringEvent(msg));
+    }
+
+    // All the methods below run on the worker thread
+    private void onLoadSoundEffects(OnEffectsLoadCompleteHandler onComplete) {
+        if (mSoundPoolLoader != null) {
+            // Loading is ongoing.
+            mSoundPoolLoader.addHandler(onComplete);
+            return;
+        }
+        if (mSoundPool != null) {
+            if (onComplete != null) {
+                onComplete.run(true /*success*/);
+            }
+            return;
+        }
+
+        logEvent("effects loading started");
+        mSoundPool = new SoundPool.Builder()
+                .setMaxStreams(NUM_SOUNDPOOL_CHANNELS)
+                .setAudioAttributes(new AudioAttributes.Builder()
+                        .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                        .build())
+                .build();
+        loadTouchSoundAssets();
+
+        mSoundPoolLoader = new SoundPoolLoader();
+        mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() {
+            @Override
+            public void run(boolean success) {
+                mSoundPoolLoader = null;
+                if (!success) {
+                    Log.w(TAG, "onLoadSoundEffects(), Error while loading samples");
+                    onUnloadSoundEffects();
+                }
+            }
+        });
+        mSoundPoolLoader.addHandler(onComplete);
+
+        int resourcesToLoad = 0;
+        for (Resource res : mResources) {
+            String filePath = getResourceFilePath(res);
+            int sampleId = mSoundPool.load(filePath, 0);
+            if (sampleId > 0) {
+                res.mSampleId = sampleId;
+                res.mLoaded = false;
+                resourcesToLoad++;
+            } else {
+                logEvent("effect " + filePath + " rejected by SoundPool");
+                Log.w(TAG, "SoundPool could not load file: " + filePath);
+            }
+        }
+
+        if (resourcesToLoad > 0) {
+            sendMsg(MSG_LOAD_EFFECTS_TIMEOUT, 0, 0, null, SOUND_EFFECTS_LOAD_TIMEOUT_MS);
+        } else {
+            logEvent("effects loading completed, no effects to load");
+            mSoundPoolLoader.onComplete(true /*success*/);
+        }
+    }
+
+    void onUnloadSoundEffects() {
+        if (mSoundPool == null) {
+            return;
+        }
+        if (mSoundPoolLoader != null) {
+            mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() {
+                @Override
+                public void run(boolean success) {
+                    onUnloadSoundEffects();
+                }
+            });
+        }
+
+        logEvent("effects unloading started");
+        for (Resource res : mResources) {
+            if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL) {
+                mSoundPool.unload(res.mSampleId);
+            }
+        }
+        mSoundPool.release();
+        mSoundPool = null;
+        logEvent("effects unloading completed");
+    }
+
+    void onPlaySoundEffect(int effect, int volume) {
+        float volFloat;
+        // use default if volume is not specified by caller
+        if (volume < 0) {
+            volFloat = (float) Math.pow(10, (float) mSfxAttenuationDb / 20);
+        } else {
+            volFloat = volume / 1000.0f;
+        }
+
+        Resource res = mResources.get(mEffects[effect]);
+        if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) {
+            mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f);
+        } else {
+            MediaPlayer mediaPlayer = new MediaPlayer();
+            try {
+                String filePath = getResourceFilePath(res);
+                mediaPlayer.setDataSource(filePath);
+                mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
+                mediaPlayer.prepare();
+                mediaPlayer.setVolume(volFloat);
+                mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
+                    public void onCompletion(MediaPlayer mp) {
+                        cleanupPlayer(mp);
+                    }
+                });
+                mediaPlayer.setOnErrorListener(new OnErrorListener() {
+                    public boolean onError(MediaPlayer mp, int what, int extra) {
+                        cleanupPlayer(mp);
+                        return true;
+                    }
+                });
+                mediaPlayer.start();
+            } catch (IOException ex) {
+                Log.w(TAG, "MediaPlayer IOException: " + ex);
+            } catch (IllegalArgumentException ex) {
+                Log.w(TAG, "MediaPlayer IllegalArgumentException: " + ex);
+            } catch (IllegalStateException ex) {
+                Log.w(TAG, "MediaPlayer IllegalStateException: " + ex);
+            }
+        }
+    }
+
+    private static void cleanupPlayer(MediaPlayer mp) {
+        if (mp != null) {
+            try {
+                mp.stop();
+                mp.release();
+            } catch (IllegalStateException ex) {
+                Log.w(TAG, "MediaPlayer IllegalStateException: " + ex);
+            }
+        }
+    }
+
+    private static final String TAG_AUDIO_ASSETS = "audio_assets";
+    private static final String ATTR_VERSION = "version";
+    private static final String TAG_GROUP = "group";
+    private static final String ATTR_GROUP_NAME = "name";
+    private static final String TAG_ASSET = "asset";
+    private static final String ATTR_ASSET_ID = "id";
+    private static final String ATTR_ASSET_FILE = "file";
+
+    private static final String ASSET_FILE_VERSION = "1.0";
+    private static final String GROUP_TOUCH_SOUNDS = "touch_sounds";
+
+    private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 15000;
+
+    private String getResourceFilePath(Resource res) {
+        String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + res.mFileName;
+        if (!new File(filePath).isFile()) {
+            filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + res.mFileName;
+        }
+        return filePath;
+    }
+
+    private void loadTouchSoundAssetDefaults() {
+        int defaultResourceIdx = mResources.size();
+        mResources.add(new Resource("Effect_Tick.ogg"));
+        for (int i = 0; i < mEffects.length; i++) {
+            mEffects[i] = defaultResourceIdx;
+        }
+    }
+
+    private void loadTouchSoundAssets() {
+        XmlResourceParser parser = null;
+
+        // only load assets once.
+        if (!mResources.isEmpty()) {
+            return;
+        }
+
+        loadTouchSoundAssetDefaults();
+
+        try {
+            parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets);
+
+            XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS);
+            String version = parser.getAttributeValue(null, ATTR_VERSION);
+            boolean inTouchSoundsGroup = false;
+
+            if (ASSET_FILE_VERSION.equals(version)) {
+                while (true) {
+                    XmlUtils.nextElement(parser);
+                    String element = parser.getName();
+                    if (element == null) {
+                        break;
+                    }
+                    if (element.equals(TAG_GROUP)) {
+                        String name = parser.getAttributeValue(null, ATTR_GROUP_NAME);
+                        if (GROUP_TOUCH_SOUNDS.equals(name)) {
+                            inTouchSoundsGroup = true;
+                            break;
+                        }
+                    }
+                }
+                while (inTouchSoundsGroup) {
+                    XmlUtils.nextElement(parser);
+                    String element = parser.getName();
+                    if (element == null) {
+                        break;
+                    }
+                    if (element.equals(TAG_ASSET)) {
+                        String id = parser.getAttributeValue(null, ATTR_ASSET_ID);
+                        String file = parser.getAttributeValue(null, ATTR_ASSET_FILE);
+                        int fx;
+
+                        try {
+                            Field field = AudioManager.class.getField(id);
+                            fx = field.getInt(null);
+                        } catch (Exception e) {
+                            Log.w(TAG, "Invalid touch sound ID: " + id);
+                            continue;
+                        }
+
+                        mEffects[fx] = findOrAddResourceByFileName(file);
+                    } else {
+                        break;
+                    }
+                }
+            }
+        } catch (Resources.NotFoundException e) {
+            Log.w(TAG, "audio assets file not found", e);
+        } catch (XmlPullParserException e) {
+            Log.w(TAG, "XML parser exception reading touch sound assets", e);
+        } catch (IOException e) {
+            Log.w(TAG, "I/O exception reading touch sound assets", e);
+        } finally {
+            if (parser != null) {
+                parser.close();
+            }
+        }
+    }
+
+    private int findOrAddResourceByFileName(String fileName) {
+        for (int i = 0; i < mResources.size(); i++) {
+            if (mResources.get(i).mFileName.equals(fileName)) {
+                return i;
+            }
+        }
+        int result = mResources.size();
+        mResources.add(new Resource(fileName));
+        return result;
+    }
+
+    private Resource findResourceBySampleId(int sampleId) {
+        for (Resource res : mResources) {
+            if (res.mSampleId == sampleId) {
+                return res;
+            }
+        }
+        return null;
+    }
+
+    private class SfxWorker extends Thread {
+        SfxWorker() {
+            super("AS.SfxWorker");
+        }
+
+        @Override
+        public void run() {
+            Looper.prepare();
+            synchronized (SoundEffectsHelper.this) {
+                mSfxHandler = new SfxHandler();
+                SoundEffectsHelper.this.notify();
+            }
+            Looper.loop();
+        }
+    }
+
+    private class SfxHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_LOAD_EFFECTS:
+                    onLoadSoundEffects((OnEffectsLoadCompleteHandler) msg.obj);
+                    break;
+                case MSG_UNLOAD_EFFECTS:
+                    onUnloadSoundEffects();
+                    break;
+                case MSG_PLAY_EFFECT:
+                    onLoadSoundEffects(new OnEffectsLoadCompleteHandler() {
+                        @Override
+                        public void run(boolean success) {
+                            if (success) {
+                                onPlaySoundEffect(msg.arg1 /*effect*/, msg.arg2 /*volume*/);
+                            }
+                        }
+                    });
+                    break;
+                case MSG_LOAD_EFFECTS_TIMEOUT:
+                    if (mSoundPoolLoader != null) {
+                        mSoundPoolLoader.onTimeout();
+                    }
+                    break;
+            }
+        }
+    }
+
+    private class SoundPoolLoader implements
+            android.media.SoundPool.OnLoadCompleteListener {
+
+        private List<OnEffectsLoadCompleteHandler> mLoadCompleteHandlers =
+                new ArrayList<OnEffectsLoadCompleteHandler>();
+
+        SoundPoolLoader() {
+            // SoundPool use the current Looper when creating its message handler.
+            // Since SoundPoolLoader is created on the SfxWorker thread, SoundPool's
+            // message handler ends up running on it (it's OK to have multiple
+            // handlers on the same Looper). Thus, onLoadComplete gets executed
+            // on the worker thread.
+            mSoundPool.setOnLoadCompleteListener(this);
+        }
+
+        void addHandler(OnEffectsLoadCompleteHandler handler) {
+            if (handler != null) {
+                mLoadCompleteHandlers.add(handler);
+            }
+        }
+
+        @Override
+        public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
+            if (status == 0) {
+                int remainingToLoad = 0;
+                for (Resource res : mResources) {
+                    if (res.mSampleId == sampleId && !res.mLoaded) {
+                        logEvent("effect " + res.mFileName + " loaded");
+                        res.mLoaded = true;
+                    }
+                    if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && !res.mLoaded) {
+                        remainingToLoad++;
+                    }
+                }
+                if (remainingToLoad == 0) {
+                    onComplete(true);
+                }
+            } else {
+                Resource res = findResourceBySampleId(sampleId);
+                String filePath;
+                if (res != null) {
+                    filePath = getResourceFilePath(res);
+                } else {
+                    filePath = "with unknown sample ID " + sampleId;
+                }
+                logEvent("effect " + filePath + " loading failed, status " + status);
+                Log.w(TAG, "onLoadSoundEffects(), Error " + status + " while loading sample "
+                        + filePath);
+                onComplete(false);
+            }
+        }
+
+        void onTimeout() {
+            onComplete(false);
+        }
+
+        void onComplete(boolean success) {
+            mSoundPool.setOnLoadCompleteListener(null);
+            for (OnEffectsLoadCompleteHandler handler : mLoadCompleteHandlers) {
+                handler.run(success);
+            }
+            logEvent("effects loading " + (success ? "completed" : "failed"));
+        }
+    }
+}
diff --git a/com/android/server/audio/SystemServerAdapter.java b/com/android/server/audio/SystemServerAdapter.java
new file mode 100644
index 0000000..68893f8
--- /dev/null
+++ b/com/android/server/audio/SystemServerAdapter.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import java.util.Objects;
+
+/**
+ * Provides an adapter to access functionality reserved to components running in system_server
+ * Functionality such as sending privileged broadcasts is to be accessed through the default
+ * adapter, whereas tests can inject a no-op adapter.
+ */
+public class SystemServerAdapter {
+
+    protected final Context mContext;
+
+    protected SystemServerAdapter(@Nullable Context context) {
+        mContext = context;
+    }
+    /**
+     * Create a wrapper around privileged functionality.
+     * @return the adapter
+     */
+    static final @NonNull SystemServerAdapter getDefaultAdapter(Context context) {
+        Objects.requireNonNull(context);
+        return new SystemServerAdapter(context);
+    }
+
+    /**
+     * @return true if this is supposed to be run in system_server, false otherwise (e.g. for a
+     *     unit test)
+     */
+    public boolean isPrivileged() {
+        return true;
+    }
+
+    /**
+     * Broadcast ACTION_MICROPHONE_MUTE_CHANGED
+     */
+    public void sendMicrophoneMuteChangedIntent() {
+        mContext.sendBroadcastAsUser(
+                new Intent(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED)
+                        .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
+                UserHandle.ALL);
+    }
+
+    /**
+     * Broadcast ACTION_AUDIO_BECOMING_NOISY
+     */
+    public void sendDeviceBecomingNoisyIntent() {
+        if (mContext == null) {
+            return;
+        }
+        final Intent intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+}