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