blob: 0b10e2b7bad2abf3922e1cd8c889b01c9c8a150e [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.audio;
import static com.android.car.audio.CarAudioContext.isCriticalAudioContext;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioPolicy;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.Slog;
import com.android.car.CarLog;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
class CarAudioFocus extends AudioPolicy.AudioPolicyFocusListener {
private static final String TAG = CarLog.tagFor(CarAudioFocus.class);
private static final int FOCUS_EVENT_LOGGER_QUEUE_SIZE = 25;
private final AudioManager mAudioManager;
private final PackageManager mPackageManager;
private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction
private final LocalLog mFocusEventLogger;
private final FocusInteraction mFocusInteraction;
private final boolean mEnabledDelayedFocusRequest;
private AudioFocusInfo mDelayedRequest;
// 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
// Note that the clientId reflects the AudioManager instance and listener object (if any)
// so that one app can have more than one unique clientId by setting up distinct listeners.
// Because the listener gets only LOSS/GAIN messages, this is important for an app to do if
// it expects to request focus concurrently for different USAGEs so it knows which USAGE
// gained or lost focus at any given moment. If the SAME listener is used for requests of
// different USAGE while the earlier request is still in the focus stack (whether holding
// focus or pending), the new request will be REJECTED so as to avoid any confusion about
// the meaning of subsequent GAIN/LOSS events (which would continue to apply to the focus
// request that was already active or pending).
private final Map<String, FocusEntry> mFocusHolders = new ArrayMap<>();
private final Map<String, FocusEntry> mFocusLosers = new ArrayMap<>();
private final Object mLock = new Object();
@GuardedBy("mLock")
private boolean mIsFocusRestricted;
CarAudioFocus(AudioManager audioManager, PackageManager packageManager,
FocusInteraction focusInteraction, boolean enableDelayedFocusRequest) {
mAudioManager = audioManager;
mPackageManager = packageManager;
mFocusEventLogger = new LocalLog(FOCUS_EVENT_LOGGER_QUEUE_SIZE);
mFocusInteraction = focusInteraction;
mEnabledDelayedFocusRequest = enableDelayedFocusRequest;
}
// This has to happen after the construction to avoid a chicken and egg problem when setting up
// the AudioPolicy which must depend on this object.
public void setOwningPolicy(AudioPolicy parentPolicy) {
mAudioPolicy = parentPolicy;
}
void setRestrictFocus(boolean isFocusRestricted) {
synchronized (mLock) {
mIsFocusRestricted = isFocusRestricted;
if (mIsFocusRestricted) {
abandonNonCriticalFocusLocked();
}
}
}
@GuardedBy("mLock")
private void abandonNonCriticalFocusLocked() {
if (mEnabledDelayedFocusRequest && mDelayedRequest != null) {
int audioContext = CarAudioContext.getContextForAttributes(
mDelayedRequest.getAttributes());
if (!isCriticalAudioContext(audioContext)) {
sendFocusLossLocked(mDelayedRequest, AudioManager.AUDIOFOCUS_LOSS);
mDelayedRequest = null;
}
}
abandonNonCriticalEntriesLocked(mFocusLosers);
abandonNonCriticalEntriesLocked(mFocusHolders);
}
@GuardedBy("mLock")
private void abandonNonCriticalEntriesLocked(Map<String, FocusEntry> entries) {
List<String> clientsToRemove = new ArrayList<>();
for (FocusEntry holderEntry : entries.values()) {
if (isCriticalAudioContext(holderEntry.getAudioContext())) {
continue;
}
sendFocusLossLocked(holderEntry.getAudioFocusInfo(), AudioManager.AUDIOFOCUS_LOSS);
clientsToRemove.add(holderEntry.getAudioFocusInfo().getClientId());
}
for (int i = 0; i < clientsToRemove.size(); i++) {
String clientId = clientsToRemove.get(i);
FocusEntry removedEntry = entries.remove(clientId);
removeBlockerAndRestoreUnblockedWaitersLocked(removedEntry);
}
}
// This sends a focus loss message to the targeted requester.
private void sendFocusLossLocked(AudioFocusInfo loser, int lossType) {
int result = mAudioManager.dispatchAudioFocusChange(loser, lossType,
mAudioPolicy);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// TODO: Is this actually an error, or is it okay for an entry in the focus stack
// to NOT have a listener? If that's the case, should we even keep it in the focus
// stack?
Slog.e(TAG, "Failure to signal loss of audio focus with error: " + result);
}
logFocusEvent("sendFocusLoss for client " + loser.getClientId()
+ " with loss type " + focusEventToString(lossType)
+ " resulted in " + focusRequestResponseToString(result));
}
/** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */
// Note that we replicate most, but not all of the behaviors of the default MediaFocusControl
// engine as of Android P.
// Besides the interaction matrix which allows concurrent focus for multiple requestors, which
// is the reason for this module, we also treat repeated requests from the same clientId
// slightly differently.
// If a focus request for the same listener (clientId) is received while that listener is
// already in the focus stack, we REJECT it outright unless it is for the same USAGE.
// If it is for the same USAGE, we replace the old request with the new one.
// The default audio framework's behavior is to remove the previous entry in the stack (no-op
// if the requester is already holding focus).
@GuardedBy("mLock")
private int evaluateFocusRequestLocked(AudioFocusInfo afi) {
Slog.i(TAG, "Evaluating " + focusEventToString(afi.getGainRequest())
+ " request for client " + afi.getClientId()
+ " with usage " + afi.getAttributes().usageToString());
if (mIsFocusRestricted) {
int audioContext = CarAudioContext.getContextForAttributes(afi.getAttributes());
if (!isCriticalAudioContext(audioContext)) {
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
}
// Is this a request for premanant focus?
// AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -- Means Notifications should be denied
// AUDIOFOCUS_GAIN_TRANSIENT -- Means current focus holders should get transient loss
// AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -- Means other can duck (no loss message from us)
// NOTE: We expect that in practice it will be permanent for all media requests and
// transient for everything else, but that isn't currently an enforced requirement.
final boolean permanent =
(afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN);
final boolean allowDucking =
(afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
boolean delayFocusForCurrentRequest = false;
int requestedContext = CarAudioContext.getContextForAttributes(afi.getAttributes());
// If we happen to find entries that this new request should replace, we'll store them here.
// This happens when a client makes a second AF request on the same listener.
// After we've granted audio focus to our current request, we'll abandon these requests.
FocusEntry replacedCurrentEntry = null;
FocusEntry replacedBlockedEntry = null;
boolean allowDelayedFocus = mEnabledDelayedFocusRequest && canReceiveDelayedFocus(afi);
// We don't allow sharing listeners (client IDs) between two concurrent requests
// (because the app would have no way to know to which request a later event applied)
if (mDelayedRequest != null && afi.getClientId().equals(mDelayedRequest.getClientId())) {
int delayedRequestedContext = CarAudioContext.getContextForAttributes(
mDelayedRequest.getAttributes());
// If it is for a different context then reject
if (delayedRequestedContext != requestedContext) {
// Trivially reject a request for a different USAGE
Slog.e(TAG, String.format(
"Client %s has already delayed requested focus for %s "
+ "- cannot request focus for %s on same listener.",
mDelayedRequest.getClientId(),
mDelayedRequest.getAttributes().usageToString(),
afi.getAttributes().usageToString()));
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
}
// Scan all active and pending focus requests. If any should cause rejection of
// this new request, then we're done. Keep a list of those against whom we're exclusive
// so we can update the relationships if/when we are sure we won't get rejected.
Slog.i(TAG, "Scanning focus holders...");
final ArrayList<FocusEntry> losers = new ArrayList<FocusEntry>();
for (FocusEntry entry : mFocusHolders.values()) {
Slog.d(TAG, "Evaluating focus holder: " + entry.getClientId());
// If this request is for Notifications and a current focus holder has specified
// AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request.
// This matches the hardwired behavior in the default audio policy engine which apps
// might expect (The interaction matrix doesn't have any provision for dealing with
// override flags like this).
if ((requestedContext == CarAudioContext.NOTIFICATION)
&& (entry.getAudioFocusInfo().getGainRequest()
== AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
// We don't allow sharing listeners (client IDs) between two concurrent requests
// (because the app would have no way to know to which request a later event applied)
if (afi.getClientId().equals(entry.getAudioFocusInfo().getClientId())) {
if (entry.getAudioContext() == requestedContext) {
// This is a request from a current focus holder.
// Abandon the previous request (without sending a LOSS notification to it),
// and don't check the interaction matrix for it.
Slog.i(TAG, "Replacing accepted request from same client");
replacedCurrentEntry = entry;
continue;
} else {
// Trivially reject a request for a different USAGE
Slog.e(TAG, String.format(
"Client %s has already requested focus for %s - cannot request focus "
+ "for %s on same listener.",
entry.getClientId(),
entry.getAudioFocusInfo().getAttributes().usageToString(),
afi.getAttributes().usageToString()));
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
}
@AudioManager.FocusRequestResult int interactionResult = mFocusInteraction
.evaluateRequest(requestedContext, entry, losers, allowDucking,
allowDelayedFocus);
if (interactionResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
return interactionResult;
}
if (interactionResult == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
delayFocusForCurrentRequest = true;
}
}
Slog.i(TAG, "Scanning those who've already lost focus...");
final ArrayList<FocusEntry> blocked = new ArrayList<FocusEntry>();
for (FocusEntry entry : mFocusLosers.values()) {
Slog.i(TAG, entry.getAudioFocusInfo().getClientId());
// If this request is for Notifications and a pending focus holder has specified
// AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request
if ((requestedContext == CarAudioContext.NOTIFICATION)
&& (entry.getAudioFocusInfo().getGainRequest()
== AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
// We don't allow sharing listeners (client IDs) between two concurrent requests
// (because the app would have no way to know to which request a later event applied)
if (afi.getClientId().equals(entry.getAudioFocusInfo().getClientId())) {
if (entry.getAudioContext() == requestedContext) {
// This is a repeat of a request that is currently blocked.
// Evaluate it as if it were a new request, but note that we should remove
// the old pending request, and move it.
// We do not want to evaluate the new request against itself.
Slog.i(TAG, "Replacing pending request from same client");
replacedBlockedEntry = entry;
continue;
} else {
// Trivially reject a request for a different USAGE
Slog.e(TAG, String.format(
"Client %s has already requested focus for %s - cannot request focus "
+ "for %s on same listener.",
entry.getClientId(),
entry.getAudioFocusInfo().getAttributes().usageToString(),
afi.getAttributes().usageToString()));
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
}
@AudioManager.FocusRequestResult int interactionResult = mFocusInteraction
.evaluateRequest(requestedContext, entry, blocked, allowDucking,
allowDelayedFocus);
if (interactionResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
return interactionResult;
}
if (interactionResult == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
delayFocusForCurrentRequest = true;
}
}
// Now that we've decided we'll grant focus, construct our new FocusEntry
FocusEntry newEntry = new FocusEntry(afi, requestedContext, mPackageManager);
// These entries have permanently lost focus as a result of this request, so they
// should be removed from all blocker lists.
ArrayList<FocusEntry> permanentlyLost = new ArrayList<>();
if (replacedCurrentEntry != null) {
mFocusHolders.remove(replacedCurrentEntry.getClientId());
permanentlyLost.add(replacedCurrentEntry);
}
if (replacedBlockedEntry != null) {
mFocusLosers.remove(replacedBlockedEntry.getClientId());
permanentlyLost.add(replacedBlockedEntry);
}
// Now that we're sure we'll accept this request, update any requests which we would
// block but are already out of focus but waiting to come back
for (FocusEntry entry : blocked) {
// If we're out of focus it must be because somebody is blocking us
assert !entry.isUnblocked();
if (permanent) {
// This entry has now lost focus forever
sendFocusLossLocked(entry.getAudioFocusInfo(), AudioManager.AUDIOFOCUS_LOSS);
entry.setDucked(false);
final FocusEntry deadEntry = mFocusLosers.remove(
entry.getAudioFocusInfo().getClientId());
assert deadEntry != null;
permanentlyLost.add(entry);
} else {
if (!allowDucking && entry.isDucked()) {
// This entry was previously allowed to duck, but can no longer do so.
Slog.i(TAG, "Converting duckable loss to non-duckable for "
+ entry.getClientId());
sendFocusLossLocked(entry.getAudioFocusInfo(),
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
entry.setDucked(false);
}
// Note that this new request is yet one more reason we can't (yet) have focus
entry.addBlocker(newEntry);
}
}
// Notify and update any requests which are now losing focus as a result of the new request
for (FocusEntry entry : losers) {
// If we have focus (but are about to loose it), nobody should be blocking us yet
assert entry.isUnblocked();
int lossType;
if (permanent) {
lossType = AudioManager.AUDIOFOCUS_LOSS;
} else if (allowDucking && entry.receivesDuckEvents()) {
lossType = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
entry.setDucked(true);
} else {
lossType = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
}
sendFocusLossLocked(entry.getAudioFocusInfo(), lossType);
// The entry no longer holds focus, so take it out of the holders list
mFocusHolders.remove(entry.getAudioFocusInfo().getClientId());
if (permanent) {
permanentlyLost.add(entry);
} else {
// Add ourselves to the list of requests waiting to get focus back and
// note why we lost focus so we can tell when it's time to get it back
mFocusLosers.put(entry.getAudioFocusInfo().getClientId(), entry);
entry.addBlocker(newEntry);
}
}
// Now that all new blockers have been added, clear out any other requests that have been
// permanently lost as a result of this request. Treat them as abandoned - if they're on
// any blocker lists, remove them. If any focus requests become unblocked as a result,
// re-grant them. (This can happen when a GAIN_TRANSIENT_MAY_DUCK request replaces a
// GAIN_TRANSIENT request from the same listener.)
for (FocusEntry entry : permanentlyLost) {
Slog.d(TAG, "Cleaning up entry " + entry.getClientId());
removeBlockerAndRestoreUnblockedWaitersLocked(entry);
}
if (delayFocusForCurrentRequest) {
swapDelayedAudioFocusRequestLocked(afi);
return AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
}
mFocusHolders.put(afi.getClientId(), newEntry);
Slog.i(TAG, "AUDIOFOCUS_REQUEST_GRANTED");
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
@Override
public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {
int response;
AudioPolicy policy;
AudioFocusInfo replacedDelayedAudioFocusInfo = null;
synchronized (mLock) {
policy = mAudioPolicy;
response = evaluateFocusRequestLocked(afi);
}
// Post our reply for delivery to the original focus requester
mAudioManager.setFocusRequestResult(afi, response, policy);
logFocusEvent("onAudioFocusRequest for client " + afi.getClientId()
+ " with gain type " + focusEventToString(afi.getGainRequest())
+ " resulted in " + focusRequestResponseToString(response));
}
private void swapDelayedAudioFocusRequestLocked(AudioFocusInfo afi) {
// If we are swapping to a different client then send the focus loss signal
if (mDelayedRequest != null
&& !afi.getClientId().equals(mDelayedRequest.getClientId())) {
sendFocusLossLocked(mDelayedRequest, AudioManager.AUDIOFOCUS_LOSS);
}
mDelayedRequest = afi;
}
private boolean canReceiveDelayedFocus(AudioFocusInfo afi) {
if (afi.getGainRequest() != AudioManager.AUDIOFOCUS_GAIN) {
return false;
}
return (afi.getFlags() & AudioManager.AUDIOFOCUS_FLAG_DELAY_OK)
== AudioManager.AUDIOFOCUS_FLAG_DELAY_OK;
}
/**
* @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes)
* Note that we'll get this call for a focus holder that dies while in the focus stack, so
* we don't need to watch for death notifications directly.
* */
@Override
public void onAudioFocusAbandon(AudioFocusInfo afi) {
logFocusEvent("onAudioFocusAbandon for client " + afi.getClientId());
synchronized (mLock) {
FocusEntry deadEntry = removeFocusEntryLocked(afi);
if (deadEntry != null) {
removeBlockerAndRestoreUnblockedWaitersLocked(deadEntry);
} else {
removeDelayedAudioFocusRequestLocked(afi);
}
}
}
private void removeDelayedAudioFocusRequestLocked(AudioFocusInfo afi) {
if (mDelayedRequest != null && afi.getClientId().equals(mDelayedRequest.getClientId())) {
mDelayedRequest = null;
}
}
/**
* Remove Focus entry from focus holder or losers entry lists
* @param afi Audio Focus Info to remove
* @return Removed Focus Entry
*/
private FocusEntry removeFocusEntryLocked(AudioFocusInfo afi) {
Slog.i(TAG, "removeFocusEntry " + afi.getClientId());
// Remove this entry from our active or pending list
FocusEntry deadEntry = mFocusHolders.remove(afi.getClientId());
if (deadEntry == null) {
deadEntry = mFocusLosers.remove(afi.getClientId());
if (deadEntry == null) {
// Caller is providing an unrecognzied clientId!?
Slog.w(TAG, "Audio focus abandoned by unrecognized client id: "
+ afi.getClientId());
// This probably means an app double released focused for some reason. One
// harmless possibility is a race between an app being told it lost focus and the
// app voluntarily abandoning focus. More likely the app is just sloppy. :)
// The more nefarious possibility is that the clientId is actually corrupted
// somehow, in which case we might have a real focus entry that we're going to fail
// to remove. If that were to happen, I'd expect either the app to swallow it
// silently, or else take unexpected action (eg: resume playing spontaneously), or
// else to see "Failure to signal ..." gain/loss error messages in the log from
// this module when a focus change tries to take action on a truly zombie entry.
}
}
return deadEntry;
}
private void removeBlockerAndRestoreUnblockedWaitersLocked(FocusEntry deadEntry) {
attemptToGainFocusForDelayedAudioFocusRequest();
removeBlockerAndRestoreUnblockedFocusLosersLocked(deadEntry);
}
private void attemptToGainFocusForDelayedAudioFocusRequest() {
if (!mEnabledDelayedFocusRequest || mDelayedRequest == null) {
return;
}
int delayedFocusRequestResults = evaluateFocusRequestLocked(mDelayedRequest);
if (delayedFocusRequestResults == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
FocusEntry focusEntry = mFocusHolders.get(mDelayedRequest.getClientId());
mDelayedRequest = null;
if (dispatchFocusGainedLocked(focusEntry.getAudioFocusInfo())
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Slog.e(TAG,
"Failure to signal gain of audio focus gain for "
+ "delayed focus clientId " + focusEntry.getClientId());
mFocusHolders.remove(focusEntry.getClientId());
removeBlockerFromBlockedFocusLosersLocked(focusEntry);
sendFocusLossLocked(focusEntry.getAudioFocusInfo(),
AudioManager.AUDIOFOCUS_LOSS);
logFocusEvent("Did not gained delayed audio focus for "
+ focusEntry.getClientId());
}
}
}
/**
* Removes the dead entry from blocked waiters but does not send focus gain signal
*/
private void removeBlockerFromBlockedFocusLosersLocked(FocusEntry deadEntry) {
// Remove this entry from the blocking list of any pending requests
Iterator<FocusEntry> it = mFocusLosers.values().iterator();
while (it.hasNext()) {
FocusEntry entry = it.next();
// Remove the retiring entry from all blocker lists
entry.removeBlocker(deadEntry);
}
}
/**
* Removes the dead entry from blocked waiters and sends focus gain signal
*/
private void removeBlockerAndRestoreUnblockedFocusLosersLocked(FocusEntry deadEntry) {
// Remove this entry from the blocking list of any pending requests
Iterator<FocusEntry> it = mFocusLosers.values().iterator();
while (it.hasNext()) {
FocusEntry entry = it.next();
// Remove the retiring entry from all blocker lists
entry.removeBlocker(deadEntry);
// Any entry whose blocking list becomes empty should regain focus
if (entry.isUnblocked()) {
Slog.i(TAG, "Restoring unblocked entry " + entry.getClientId());
// Pull this entry out of the focus losers list
it.remove();
// Add it back into the focus holders list
mFocusHolders.put(entry.getClientId(), entry);
dispatchFocusGainedLocked(entry.getAudioFocusInfo());
}
}
}
/**
* Dispatch focus gain
* @param afi Audio focus info
* @return AudioManager.AUDIOFOCUS_REQUEST_GRANTED if focus is dispatched successfully
*/
private int dispatchFocusGainedLocked(AudioFocusInfo afi) {
// Send the focus (re)gain notification
int result = mAudioManager.dispatchAudioFocusChange(
afi,
afi.getGainRequest(),
mAudioPolicy);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// TODO: Is this actually an error, or is it okay for an entry in the focus
// stack to NOT have a listener? If that's the case, should we even keep
// it in the focus stack?
Slog.e(TAG, "Failure to signal gain of audio focus with error: " + result);
}
logFocusEvent("dispatchFocusGainedLocked for client " + afi.getClientId()
+ " with gain type " + focusEventToString(afi.getGainRequest())
+ " resulted in " + focusRequestResponseToString(result));
return result;
}
/**
* Query the current list of focus loser for uid
* @param uid uid to query current focus loser
* @return list of current focus losers for uid
*/
ArrayList<AudioFocusInfo> getAudioFocusLosersForUid(int uid) {
return getAudioFocusListForUid(uid, mFocusLosers);
}
/**
* Query the current list of focus holders for uid
* @param uid uid to query current focus holders
* @return list of current focus holders that for uid
*/
ArrayList<AudioFocusInfo> getAudioFocusHoldersForUid(int uid) {
return getAudioFocusListForUid(uid, mFocusHolders);
}
List<AudioFocusInfo> getAudioFocusHolders() {
List<AudioFocusInfo> focusHolders = new ArrayList<>();
synchronized (mLock) {
for (FocusEntry entry : mFocusHolders.values()) {
focusHolders.add(entry.getAudioFocusInfo());
}
return focusHolders;
}
}
/**
* Query input list for matching uid
* @param uid uid to match in map
* @param mapToQuery map to query for uid info
* @return list of audio focus info that match uid
*/
private ArrayList<AudioFocusInfo> getAudioFocusListForUid(int uid,
Map<String, FocusEntry> mapToQuery) {
ArrayList<AudioFocusInfo> matchingInfoList = new ArrayList<>();
synchronized (mLock) {
for (String clientId : mapToQuery.keySet()) {
AudioFocusInfo afi = mapToQuery.get(clientId).getAudioFocusInfo();
if (afi.getClientUid() == uid) {
matchingInfoList.add(afi);
}
}
}
return matchingInfoList;
}
/**
* Remove the audio focus info, if entry is still active
* dispatch lose focus transient to listeners
* @param afi Audio Focus info to remove
*/
void removeAudioFocusInfoAndTransientlyLoseFocus(AudioFocusInfo afi) {
synchronized (mLock) {
FocusEntry deadEntry = removeFocusEntryLocked(afi);
if (deadEntry != null) {
sendFocusLossLocked(deadEntry.getAudioFocusInfo(),
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
removeBlockerAndRestoreUnblockedWaitersLocked(deadEntry);
}
}
}
/**
* Reevaluate focus request and regain focus
* @param afi audio focus info to reevaluate
* @return AudioManager.AUDIOFOCUS_REQUEST_GRANTED if focus is granted
*/
int reevaluateAndRegainAudioFocus(AudioFocusInfo afi) {
int results;
synchronized (mLock) {
results = evaluateFocusRequestLocked(afi);
if (results == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
results = dispatchFocusGainedLocked(afi);
}
}
return results;
}
public void dump(IndentingPrintWriter writer) {
synchronized (mLock) {
writer.println("*CarAudioFocus*");
writer.increaseIndent();
writer.printf("Is focus restricted? %b\n", mIsFocusRestricted);
writer.println();
mFocusInteraction.dump(writer);
writer.println("Current Focus Holders:");
writer.increaseIndent();
for (String clientId : mFocusHolders.keySet()) {
mFocusHolders.get(clientId).dump(writer);
}
writer.decreaseIndent();
writer.println("Transient Focus Losers:");
writer.increaseIndent();
for (String clientId : mFocusLosers.keySet()) {
mFocusLosers.get(clientId).dump(writer);
}
writer.decreaseIndent();
writer.printf("Queued Delayed Focus: %s\n",
mDelayedRequest == null ? "None" : mDelayedRequest.getClientId());
writer.println("Focus Events:");
writer.increaseIndent();
mFocusEventLogger.dump(writer);
writer.decreaseIndent();
writer.decreaseIndent();
}
}
private static String focusEventToString(int focusEvent) {
switch (focusEvent) {
case AudioManager.AUDIOFOCUS_GAIN:
return "GAIN";
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
return "GAIN_TRANSIENT";
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
return "GAIN_TRANSIENT_EXCLUSIVE";
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
return "GAIN_TRANSIENT_MAY_DUCK";
case AudioManager.AUDIOFOCUS_LOSS:
return "LOSS";
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
return "LOSS_TRANSIENT";
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
return "LOSS_TRANSIENT_CAN_DUCK";
default:
return "unknown event " + focusEvent;
}
}
private static String focusRequestResponseToString(int response) {
if (response == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
return "REQUEST_GRANTED";
} else if (response == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
return "REQUEST_FAILED";
}
return "REQUEST_DELAYED";
}
private void logFocusEvent(String log) {
mFocusEventLogger.log(log);
Slog.i(TAG, log);
}
/**
* Returns the focus interaction for this car focus instance.
*/
public FocusInteraction getFocusInteraction() {
return mFocusInteraction;
}
}