/* * 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 static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 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.car.internal.ExcludeFromCodeCoverageGeneratedReport; 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 mFocusHolders = new ArrayMap<>(); private final Map 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 entries) { List 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 losers = new ArrayList(); 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 blocked = new ArrayList(); 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 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 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 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 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 getAudioFocusHoldersForUid(int uid) { return getAudioFocusListForUid(uid, mFocusHolders); } List getAudioFocusHolders() { List 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 getAudioFocusListForUid(int uid, Map mapToQuery) { ArrayList 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; } @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) 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; } }