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); + } + } +}