blob: 93ffe835338f3de06784b455de2e9c2f5d5c7a88 [file] [log] [blame]
/*
* 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(FocusRequester winner, 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(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");
}