blob: e6c4abfa2086119c8067c74b358ecc03f947c77e [file] [log] [blame]
/*
* 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.ContentResolver;
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.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
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;
/**
* set to true so the framework enforces fading out apps that lose audio focus in a
* non-transient way.
*/
static final boolean ENFORCE_FADEOUT_FOR_FOCUS_LOSS = 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;
final ContentResolver cr = mContext.getContentResolver();
mMultiAudioFocusEnabled = Settings.System.getIntForUser(cr,
Settings.System.MULTI_AUDIO_FOCUS_ENABLED, 0, cr.getUserId()) != 0;
initFocusThreading();
}
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 restoreVShapedPlayers(@NonNull FocusRequester winner) {
mFocusEnforcer.restoreVShapedPlayers(winner);
}
@Override
public void mutePlayersForCall(int[] usagesToMute) {
mFocusEnforcer.mutePlayersForCall(usagesToMute);
}
@Override
public void unmutePlayersForCall() {
mFocusEnforcer.unmutePlayersForCall();
}
@Override
public boolean fadeOutPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser) {
return mFocusEnforcer.fadeOutPlayers(winner, loser);
}
@Override
public void forgetUid(int uid) {
mFocusEnforcer.forgetUid(uid);
}
//==========================================================================================
// 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) {
AudioFocusInfo abandonSource = null;
// 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.maybeRelease();
if (notifyFocusFollowers) {
abandonSource = fr.toAudioFocusInfo();
}
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();
if (notifyFocusFollowers) {
abandonSource = fr.toAudioFocusInfo();
}
// stack entry not used anymore, clear references
fr.maybeRelease();
}
}
}
// focus followers still want to know focus was abandoned, handled as a loss
if (abandonSource != null) {
abandonSource.clearLossReceived();
notifyExtPolicyFocusLoss_syncAf(abandonSource, false);
}
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);
mEventLogger.log(new AudioEventLogger.StringEvent(
"focus requester:" + fr.getClientId()
+ " in uid:" + fr.getClientUid()
+ " pack:" + fr.getPackageName()
+ " died"));
notifyExtPolicyFocusLoss_syncAf(fr.toAudioFocusInfo(), false);
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 removeFocusEntryForExtPolicyOnDeath(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();
mEventLogger.log(new AudioEventLogger.StringEvent(
"focus requester:" + fr.getClientId()
+ " in uid:" + fr.getClientUid()
+ " pack:" + fr.getPackageName()
+ " died"));
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) {
removeFocusEntryForExtPolicyOnDeath(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;
if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId());
} else {
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.
* @param testUid ignored if flags is not AudioManager.AUDIOFOCUS_FLAG_TEST (strictly equals to)
* otherwise the UID being injected for testing
* @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, int testUid) {
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();
// when using the test API, a fake UID can be injected (testUid is ignored otherwise)
// note that the test on flags is not a mask test on purpose, AUDIOFOCUS_FLAG_TEST is
// supposed to be alone in bitfield
final int uid = (flags == AudioManager.AUDIOFOCUS_FLAG_TEST)
? testUid : Binder.getCallingUid();
mEventLogger.log((new AudioEventLogger.StringEvent(
"requestAudioFocus() from uid/pid " + uid
+ "/" + Binder.getCallingPid()
+ " AA=" + aa.usageToString() + "/" + aa.contentTypeToString()
+ " 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 ((flags != AudioManager.AUDIOFOCUS_FLAG_TEST)
// note we're using the real uid for appOp evaluation
&& (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, uid,
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, uid, 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;
final ContentResolver cr = mContext.getContentResolver();
Settings.System.putIntForUser(cr,
Settings.System.MULTI_AUDIO_FOCUS_ENABLED, enabled ? 1 : 0, cr.getUserId());
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;
}
/*package*/ long getFadeOutDurationOnFocusLossMillis(AudioAttributes aa) {
if (!ENFORCE_FADEOUT_FOR_FOCUS_LOSS) {
return 0;
}
return FadeOutManager.getFadeOutDurationOnFocusLossMillis(aa);
}
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("------------------------------");
}
}
//=================================================================
// Async focus events
void postDelayedLossAfterFade(FocusRequester focusLoser, long delayMs) {
if (DEBUG) {
Log.v(TAG, "postDelayedLossAfterFade loser=" + focusLoser.getPackageName());
}
mFocusHandler.sendMessageDelayed(
mFocusHandler.obtainMessage(MSG_L_FOCUS_LOSS_AFTER_FADE, focusLoser),
FadeOutManager.FADE_OUT_DURATION_MS);
}
//=================================================================
// Message handling
private Handler mFocusHandler;
private HandlerThread mFocusThread;
/**
* dispatch a focus loss after an app has been faded out. Focus loser is to be released
* after dispatch as it has already left the stack
* args:
* msg.obj: the audio focus loser
* type:FocusRequester
*/
private static final int MSG_L_FOCUS_LOSS_AFTER_FADE = 1;
private void initFocusThreading() {
mFocusThread = new HandlerThread(TAG);
mFocusThread.start();
mFocusHandler = new Handler(mFocusThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_L_FOCUS_LOSS_AFTER_FADE:
if (DEBUG) {
Log.d(TAG, "MSG_L_FOCUS_LOSS_AFTER_FADE loser="
+ ((FocusRequester) msg.obj).getPackageName());
}
synchronized (mAudioFocusLock) {
final FocusRequester loser = (FocusRequester) msg.obj;
if (loser.isInFocusLossLimbo()) {
loser.dispatchFocusChange(AudioManager.AUDIOFOCUS_LOSS);
loser.release();
mFocusEnforcer.forgetUid(loser.getClientUid());
}
}
break;
default:
break;
}
}
};
}
}