/*
|
* 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();
|
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(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*/);
|
}
|
}
|
|
// 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);
|
}
|
}
|
|
/**
|
* 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");
|
}
|