• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.audio;
17 
18 import static android.car.builtin.media.AudioManagerHelper.isCallFocusRequestClientId;
19 import static android.car.builtin.media.AudioManagerHelper.usageToString;
20 import static android.media.AudioManager.AUDIOFOCUS_FLAG_DELAY_OK;
21 import static android.media.AudioManager.AUDIOFOCUS_GAIN;
22 import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
23 import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;
24 import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
25 import static android.media.AudioManager.AUDIOFOCUS_LOSS;
26 import static android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
27 import static android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
28 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
29 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_FAILED;
30 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
31 
32 import static com.android.car.audio.CarAudioContext.isCriticalAudioAudioAttribute;
33 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
34 
35 import android.car.builtin.util.Slogf;
36 import android.car.media.CarVolumeGroupInfo;
37 import android.car.oem.AudioFocusEntry;
38 import android.car.oem.OemCarAudioFocusEvaluationRequest;
39 import android.car.oem.OemCarAudioFocusResult;
40 import android.content.pm.PackageManager;
41 import android.media.AudioAttributes;
42 import android.media.AudioFocusInfo;
43 import android.media.AudioManager;
44 import android.media.audiopolicy.AudioPolicy;
45 import android.util.ArrayMap;
46 import android.util.Log;
47 
48 import com.android.car.CarLocalServices;
49 import com.android.car.CarLog;
50 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
51 import com.android.car.internal.util.IndentingPrintWriter;
52 import com.android.car.internal.util.LocalLog;
53 import com.android.car.oem.CarOemProxyService;
54 import com.android.internal.annotations.GuardedBy;
55 
56 import java.util.ArrayList;
57 import java.util.Iterator;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Objects;
61 
62 class CarAudioFocus extends AudioPolicy.AudioPolicyFocusListener {
63 
64     private static final String TAG = CarLog.tagFor(CarAudioFocus.class);
65 
66     private static final int FOCUS_EVENT_LOGGER_QUEUE_SIZE = 25;
67 
68     private final AudioManager mAudioManager;
69     private final PackageManager mPackageManager;
70     private final CarVolumeInfoWrapper mCarVolumeInfoWrapper;
71     private final int mAudioZoneId;
72     private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction
73 
74     private final LocalLog mFocusEventLogger;
75 
76     private final FocusInteraction mFocusInteraction;
77 
78     private final CarAudioContext mCarAudioContext;
79 
80     private AudioFocusInfo mDelayedRequest;
81 
82     // We keep track of all the focus requesters in this map, with their clientId as the key.
83     // This is used both for focus dispatch and death handling
84     // Note that the clientId reflects the AudioManager instance and listener object (if any)
85     // so that one app can have more than one unique clientId by setting up distinct listeners.
86     // Because the listener gets only LOSS/GAIN messages, this is important for an app to do if
87     // it expects to request focus concurrently for different USAGEs so it knows which USAGE
88     // gained or lost focus at any given moment.  If the SAME listener is used for requests of
89     // different USAGE while the earlier request is still in the focus stack (whether holding
90     // focus or pending), the new request will be REJECTED so as to avoid any confusion about
91     // the meaning of subsequent GAIN/LOSS events (which would continue to apply to the focus
92     // request that was already active or pending).
93     private final ArrayMap<String, FocusEntry> mFocusHolders = new ArrayMap<>();
94     private final ArrayMap<String, FocusEntry> mFocusLosers = new ArrayMap<>();
95 
96     private final Object mLock = new Object();
97 
98     @GuardedBy("mLock")
99     private boolean mIsFocusRestricted;
100 
CarAudioFocus(AudioManager audioManager, PackageManager packageManager, FocusInteraction focusInteraction, CarAudioContext carAudioContext, CarVolumeInfoWrapper volumeInfoWrapper, int zoneId)101     CarAudioFocus(AudioManager audioManager, PackageManager packageManager,
102             FocusInteraction focusInteraction, CarAudioContext carAudioContext,
103             CarVolumeInfoWrapper volumeInfoWrapper, int zoneId) {
104         mAudioManager = Objects.requireNonNull(audioManager, "Audio manager can not be null");
105         mPackageManager = Objects.requireNonNull(packageManager, "Package manager can not null");
106         mFocusEventLogger = new LocalLog(FOCUS_EVENT_LOGGER_QUEUE_SIZE);
107         mFocusInteraction = Objects.requireNonNull(focusInteraction,
108                 "Focus interactions can not be null");
109         mCarAudioContext = Objects.requireNonNull(carAudioContext,
110                 "Car audio context can not be null");
111         mCarVolumeInfoWrapper = Objects.requireNonNull(volumeInfoWrapper,
112                 "Car volume info can not be null");
113         mAudioZoneId = zoneId;
114     }
115 
116 
117     // This has to happen after the construction to avoid a chicken and egg problem when setting up
118     // the AudioPolicy which must depend on this object.
setOwningPolicy(AudioPolicy parentPolicy)119     public void setOwningPolicy(AudioPolicy parentPolicy) {
120         mAudioPolicy = parentPolicy;
121     }
122 
setRestrictFocus(boolean isFocusRestricted)123     void setRestrictFocus(boolean isFocusRestricted) {
124         synchronized (mLock) {
125             mIsFocusRestricted = isFocusRestricted;
126             if (mIsFocusRestricted) {
127                 abandonNonCriticalFocusLocked();
128             }
129         }
130     }
131 
132     @GuardedBy("mLock")
abandonNonCriticalFocusLocked()133     private void abandonNonCriticalFocusLocked() {
134         if (mDelayedRequest != null) {
135             if (!isCriticalAudioAudioAttribute(mDelayedRequest.getAttributes())) {
136                 logFocusEvent(
137                         "abandonNonCriticalFocusLocked abandoning non critical delayed request "
138                                 + mDelayedRequest);
139                 sendFocusLossLocked(mDelayedRequest, AUDIOFOCUS_LOSS);
140                 mDelayedRequest = null;
141             }
142         }
143 
144         abandonNonCriticalEntriesLocked(mFocusLosers);
145         abandonNonCriticalEntriesLocked(mFocusHolders);
146     }
147 
148     @GuardedBy("mLock")
abandonNonCriticalEntriesLocked(Map<String, FocusEntry> entries)149     private void abandonNonCriticalEntriesLocked(Map<String, FocusEntry> entries) {
150         List<String> clientsToRemove = new ArrayList<>();
151         for (FocusEntry holderEntry : entries.values()) {
152             if (isCriticalAudioAudioAttribute(holderEntry.getAudioFocusInfo().getAttributes())) {
153                 Slogf.i(TAG, "abandonNonCriticalEntriesLocked keeping critical focus "
154                         + holderEntry);
155                 continue;
156             }
157 
158             sendFocusLossLocked(holderEntry.getAudioFocusInfo(), AUDIOFOCUS_LOSS);
159             clientsToRemove.add(holderEntry.getAudioFocusInfo().getClientId());
160         }
161 
162         for (int i = 0; i < clientsToRemove.size(); i++) {
163             String clientId = clientsToRemove.get(i);
164             FocusEntry removedEntry = entries.remove(clientId);
165             removeBlockerAndRestoreUnblockedWaitersLocked(removedEntry);
166         }
167     }
168 
169     // This sends a focus loss message to the targeted requester.
sendFocusLossLocked(AudioFocusInfo loser, int lossType)170     private void sendFocusLossLocked(AudioFocusInfo loser, int lossType) {
171         int result = mAudioManager.dispatchAudioFocusChange(loser, lossType,
172                 mAudioPolicy);
173         if (result != AUDIOFOCUS_REQUEST_GRANTED) {
174             // TODO:  Is this actually an error, or is it okay for an entry in the focus stack
175             // to NOT have a listener?  If that's the case, should we even keep it in the focus
176             // stack?
177             Slogf.e(TAG, "Failure to signal loss of audio focus with error: " + result);
178         }
179 
180         logFocusEvent("sendFocusLoss for client " + loser.getClientId()
181                 + " with loss type " + focusEventToString(lossType)
182                 + " resulted in " + focusRequestResponseToString(result));
183     }
184 
185     /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */
186     // Note that we replicate most, but not all of the behaviors of the default MediaFocusControl
187     // engine as of Android P.
188     // Besides the interaction matrix which allows concurrent focus for multiple requestors, which
189     // is the reason for this module, we also treat repeated requests from the same clientId
190     // slightly differently.
191     // If a focus request for the same listener (clientId) is received while that listener is
192     // already in the focus stack, we REJECT it outright unless it is for the same USAGE.
193     // If it is for the same USAGE, we replace the old request with the new one.
194     // The default audio framework's behavior is to remove the previous entry in the stack (no-op
195     // if the requester is already holding focus).
196     @GuardedBy("mLock")
evaluateFocusRequestLocked(AudioFocusInfo afi)197     private int evaluateFocusRequestLocked(AudioFocusInfo afi) {
198         Slogf.i(TAG, "Evaluating " + focusEventToString(afi.getGainRequest())
199                 + " request for client " + afi.getClientId()
200                 + " with usage " + usageToString(afi.getAttributes().getUsage()));
201 
202         if (mIsFocusRestricted) {
203             if (!isCriticalAudioAudioAttribute(afi.getAttributes())) {
204                 return AUDIOFOCUS_REQUEST_FAILED;
205             }
206         }
207 
208         // Is this a request for permanent focus?
209         // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -- Means Notifications should be denied
210         // AUDIOFOCUS_GAIN_TRANSIENT -- Means current focus holders should get transient loss
211         // AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -- Means other can duck (no loss message from us)
212         // NOTE:  We expect that in practice it will be permanent for all media requests and
213         //        transient for everything else, but that isn't currently an enforced requirement.
214         boolean permanent = (afi.getGainRequest() == AUDIOFOCUS_GAIN);
215         boolean allowDucking = (afi.getGainRequest() == AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
216 
217         int requestedContext = mCarAudioContext.getContextForAttributes(afi.getAttributes());
218 
219         // We don't allow sharing listeners (client IDs) between two concurrent requests
220         // (because the app would have no way to know to which request a later event applied)
221         if (mDelayedRequest != null && afi.getClientId().equals(mDelayedRequest.getClientId())) {
222             int delayedRequestedContext = mCarAudioContext.getContextForAttributes(
223                     mDelayedRequest.getAttributes());
224             // If it is for a different context then reject
225             if (delayedRequestedContext != requestedContext) {
226                 // Trivially reject a request for a different USAGE
227                 Slogf.e(TAG, "Client %s has already delayed requested focus for %s - cannot request"
228                         + " focus for %s on same listener.", mDelayedRequest.getClientId(),
229                         usageToString(mDelayedRequest.getAttributes().getUsage()),
230                         usageToString(afi.getAttributes().getUsage()));
231                 return AUDIOFOCUS_REQUEST_FAILED;
232             }
233         }
234 
235         // These entries have permanently lost focus as a result of this request, so they
236         // should be removed from all blocker lists.
237         ArrayList<FocusEntry> permanentlyLost = new ArrayList<>();
238         FocusEntry replacedCurrentEntry = null;
239 
240         for (int index = 0; index < mFocusHolders.size(); index++) {
241             FocusEntry entry = mFocusHolders.valueAt(index);
242             if (Slogf.isLoggable(TAG, Log.DEBUG)) {
243                 Slogf.d(TAG, "Evaluating focus holder %s for duplicates", entry.getClientId());
244             }
245 
246             // If this request is for Notifications and a current focus holder has specified
247             // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request.
248             // This matches the hardwired behavior in the default audio policy engine which apps
249             // might expect (The interaction matrix doesn't have any provision for dealing with
250             // override flags like this).
251             if (CarAudioContext.isNotificationAudioAttribute(afi.getAttributes())
252                     && (entry.getAudioFocusInfo().getGainRequest()
253                     == AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
254                 return AUDIOFOCUS_REQUEST_FAILED;
255             }
256 
257             // We don't allow sharing listeners (client IDs) between two concurrent requests
258             // (because the app would have no way to know to which request a later event applied)
259             if (afi.getClientId().equals(entry.getAudioFocusInfo().getClientId())) {
260                 if ((entry.getAudioContext() == requestedContext)
261                         || canSwapCallOrRingerClientRequest(afi.getClientId(),
262                         entry.getAudioFocusInfo().getAttributes(), afi.getAttributes())) {
263                     // This is a request from a current focus holder.
264                     // Abandon the previous request (without sending a LOSS notification to it),
265                     // and don't check the interaction matrix for it.
266                     Slogf.i(TAG, "Replacing accepted request from same client: %s", afi);
267                     replacedCurrentEntry = entry;
268                     continue;
269                 } else {
270                     // Trivially reject a request for a different USAGE
271                     Slogf.e(TAG, "Client %s has already requested focus for %s - cannot request "
272                                     + "focus for %s on same listener.", entry.getClientId(),
273                             usageToString(entry.getAudioFocusInfo().getAttributes().getUsage()),
274                             usageToString(afi.getAttributes().getUsage()));
275                     return AUDIOFOCUS_REQUEST_FAILED;
276                 }
277             }
278         }
279 
280 
281         for (int index = 0; index < mFocusLosers.size(); index++) {
282             FocusEntry entry = mFocusLosers.valueAt(index);
283             if (Slogf.isLoggable(TAG, Log.DEBUG)) {
284                 Slogf.d(TAG, "Evaluating focus loser %s for duplicates", entry.getClientId());
285             }
286 
287             // If this request is for Notifications and a pending focus holder has specified
288             // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request
289             if ((CarAudioContext.isNotificationAudioAttribute(afi.getAttributes()))
290                     && (entry.getAudioFocusInfo().getGainRequest()
291                     == AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
292                 return AUDIOFOCUS_REQUEST_FAILED;
293             }
294 
295             // We don't allow sharing listeners (client IDs) between two concurrent requests
296             // (because the app would have no way to know to which request a later event applied)
297             if (afi.getClientId().equals(entry.getAudioFocusInfo().getClientId())) {
298                 if (entry.getAudioContext() == requestedContext) {
299                     // This is a repeat of a request that is currently blocked.
300                     // Evaluate it as if it were a new request, but note that we should remove
301                     // the old pending request, and move it.
302                     // We do not want to evaluate the new request against itself.
303                     Slogf.i(TAG, "Replacing pending request from same client id: %s", afi);
304                     replacedCurrentEntry = entry;
305                     continue;
306                 } else {
307                     // Trivially reject a request for a different USAGE
308                     Slogf.e(TAG, "Client %s has already requested focus for %s - cannot request "
309                                     + "focus for %s on same listener.", entry.getClientId(),
310                             usageToString(entry.getAudioFocusInfo().getAttributes().getUsage()),
311                             usageToString(afi.getAttributes().getUsage()));
312                     return AUDIOFOCUS_REQUEST_FAILED;
313                 }
314             }
315         }
316 
317         OemCarAudioFocusResult evaluationResults =
318                 evaluateFocusRequestLocked(replacedCurrentEntry, afi);
319 
320         if (evaluationResults.equals(OemCarAudioFocusResult.EMPTY_OEM_CAR_AUDIO_FOCUS_RESULTS)) {
321             return AUDIOFOCUS_REQUEST_FAILED;
322         }
323 
324         if (evaluationResults.getAudioFocusResult() == AUDIOFOCUS_REQUEST_FAILED
325                 || evaluationResults.getAudioFocusEntry() == null) {
326             return AUDIOFOCUS_REQUEST_FAILED;
327         }
328 
329         // Now that we've decided we'll grant focus, we should remove replaced entry
330         // The current request will replaced the entry
331         if (replacedCurrentEntry != null) {
332             mFocusHolders.remove(replacedCurrentEntry.getClientId());
333             mFocusLosers.remove(replacedCurrentEntry.getClientId());
334             permanentlyLost.add(replacedCurrentEntry);
335         }
336 
337         // Now that we've decided we'll grant focus, construct our new FocusEntry
338         AudioFocusEntry focusEntry = evaluationResults.getAudioFocusEntry();
339         FocusEntry newEntry = new FocusEntry(focusEntry.getAudioFocusInfo(),
340                 focusEntry.getAudioContextId(), mPackageManager);
341 
342         // Now that we're sure we'll accept this request, update any requests which we would
343         // block but are already out of focus but waiting to come back
344         List<AudioFocusEntry> blocked = evaluationResults.getNewlyBlockedAudioFocusEntries();
345         for (int index = 0; index < blocked.size(); index++) {
346             AudioFocusEntry newlyBlocked = blocked.get(index);
347             FocusEntry entry = mFocusLosers.get(newlyBlocked.getAudioFocusInfo().getClientId());
348             // If we're out of focus it must be because somebody is blocking us
349             assert !entry.isUnblocked();
350 
351             if (permanent) {
352                 // This entry has now lost focus forever
353                 sendFocusLossLocked(entry.getAudioFocusInfo(), AUDIOFOCUS_LOSS);
354                 entry.setDucked(false);
355                 FocusEntry deadEntry = mFocusLosers.remove(
356                         entry.getAudioFocusInfo().getClientId());
357                 assert deadEntry != null;
358                 permanentlyLost.add(entry);
359             } else {
360                 if (!allowDucking && entry.isDucked()) {
361                     // This entry was previously allowed to duck, but can no longer do so.
362                     Slogf.i(TAG, "Converting duckable loss to non-duckable for "
363                             + entry.getClientId());
364                     sendFocusLossLocked(entry.getAudioFocusInfo(), AUDIOFOCUS_LOSS_TRANSIENT);
365                     entry.setDucked(false);
366                 }
367                 // Note that this new request is yet one more reason we can't (yet) have focus
368                 entry.addBlocker(newEntry);
369             }
370         }
371 
372         // Notify and update any requests which are now losing focus as a result of the new request
373         List<AudioFocusEntry> loss = evaluationResults.getNewlyLostAudioFocusEntries();
374         for (int index = 0; index < loss.size(); index++) {
375             AudioFocusEntry newlyLoss = loss.get(index);
376             FocusEntry entry = mFocusHolders.get(newlyLoss.getAudioFocusInfo().getClientId());
377             // If we have focus (but are about to loose it), nobody should be blocking us yet
378             assert entry.isUnblocked();
379 
380             int lossType;
381             if (permanent) {
382                 lossType = AUDIOFOCUS_LOSS;
383             } else if (allowDucking && entry.receivesDuckEvents()) {
384                 lossType = AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
385                 entry.setDucked(true);
386             } else {
387                 lossType = AUDIOFOCUS_LOSS_TRANSIENT;
388             }
389             sendFocusLossLocked(entry.getAudioFocusInfo(), lossType);
390 
391             // The entry no longer holds focus, so take it out of the holders list
392             mFocusHolders.remove(entry.getAudioFocusInfo().getClientId());
393 
394             if (permanent) {
395                 permanentlyLost.add(entry);
396             } else {
397                 // Add ourselves to the list of requests waiting to get focus back and
398                 // note why we lost focus so we can tell when it's time to get it back
399                 mFocusLosers.put(entry.getAudioFocusInfo().getClientId(), entry);
400                 entry.addBlocker(newEntry);
401             }
402         }
403 
404         if (evaluationResults.getAudioFocusResult() != AUDIOFOCUS_REQUEST_DELAYED) {
405             // If the entry is replacing an existing one, and if a delayed Request is pending
406             // this replaced entry is not a blocker of the delayed.
407             // So add it before reconsidering the delayed.
408             mFocusHolders.put(afi.getClientId(), newEntry);
409         }
410 
411         // Now that all new blockers have been added, clear out any other requests that have been
412         // permanently lost as a result of this request. Treat them as abandoned - if they're on
413         // any blocker lists, remove them. If any focus requests become unblocked as a result,
414         // re-grant them. (This can happen when a GAIN_TRANSIENT_MAY_DUCK request replaces a
415         // GAIN_TRANSIENT request from the same listener.)
416         for (int index = 0; index < permanentlyLost.size(); index++) {
417             FocusEntry entry = permanentlyLost.get(index);
418             if (Slogf.isLoggable(TAG, Log.DEBUG)) {
419                 Slogf.d(TAG, "Cleaning up entry " + entry.getClientId());
420             }
421             removeBlockerAndRestoreUnblockedWaitersLocked(entry);
422         }
423 
424         if (evaluationResults.getAudioFocusResult() == AUDIOFOCUS_REQUEST_DELAYED) {
425             swapDelayedAudioFocusRequestLocked(afi);
426             return AUDIOFOCUS_REQUEST_DELAYED;
427         }
428 
429         Slogf.i(TAG, "AUDIOFOCUS_REQUEST_GRANTED");
430         return AUDIOFOCUS_REQUEST_GRANTED;
431     }
432 
433     @GuardedBy("mLock")
evaluateFocusRequestLocked(FocusEntry replacedCurrentEntry, AudioFocusInfo audioFocusInfo)434     private OemCarAudioFocusResult evaluateFocusRequestLocked(FocusEntry replacedCurrentEntry,
435             AudioFocusInfo audioFocusInfo) {
436 
437         return isExternalFocusEnabled()
438                 ? evaluateFocusRequestExternallyLocked(audioFocusInfo, replacedCurrentEntry) :
439                 evaluateFocusRequestInternallyLocked(audioFocusInfo, replacedCurrentEntry);
440     }
441 
442     @GuardedBy("mLock")
evaluateFocusRequestInternallyLocked( AudioFocusInfo audioFocusInfo, FocusEntry replacedCurrentEntry)443     private OemCarAudioFocusResult evaluateFocusRequestInternallyLocked(
444             AudioFocusInfo audioFocusInfo, FocusEntry replacedCurrentEntry) {
445         boolean allowDucking =
446                 (audioFocusInfo.getGainRequest() == AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
447         boolean allowDelayedFocus = canReceiveDelayedFocus(audioFocusInfo);
448 
449         int requestedContext =
450                 mCarAudioContext.getContextForAttributes(audioFocusInfo.getAttributes());
451         FocusEvaluation holdersEvaluation = evaluateAgainstFocusHoldersLocked(replacedCurrentEntry,
452                 requestedContext, allowDucking, allowDelayedFocus);
453 
454         if (holdersEvaluation.equals(FocusEvaluation.FOCUS_EVALUATION_FAILED)) {
455             return OemCarAudioFocusResult.EMPTY_OEM_CAR_AUDIO_FOCUS_RESULTS;
456         }
457 
458         FocusEvaluation losersEvaluation = evaluateAgainstFocusLosersLocked(replacedCurrentEntry,
459                 requestedContext, allowDucking, allowDelayedFocus);
460 
461         if (losersEvaluation.equals(FocusEvaluation.FOCUS_EVALUATION_FAILED)) {
462             return OemCarAudioFocusResult.EMPTY_OEM_CAR_AUDIO_FOCUS_RESULTS;
463         }
464 
465         boolean delayFocus = holdersEvaluation.mAudioFocusEvalResults == AUDIOFOCUS_REQUEST_DELAYED
466                 || losersEvaluation.mAudioFocusEvalResults == AUDIOFOCUS_REQUEST_DELAYED;
467 
468         int results = delayFocus ? AUDIOFOCUS_REQUEST_DELAYED : AUDIOFOCUS_REQUEST_GRANTED;
469 
470         AudioFocusEntry focusEntry =
471                 new AudioFocusEntry.Builder(audioFocusInfo, requestedContext,
472                 getVolumeGroupForAttribute(audioFocusInfo.getAttributes()),
473                 AudioManager.AUDIOFOCUS_GAIN).build();
474 
475         return new OemCarAudioFocusResult.Builder(
476                 convertAudioFocusEntries(holdersEvaluation.mChangedEntries),
477                 convertAudioFocusEntries(losersEvaluation.mChangedEntries),
478                 results).setAudioFocusEntry(focusEntry)
479                 .build();
480     }
481 
482     @GuardedBy("mLock")
evaluateFocusRequestExternallyLocked(AudioFocusInfo requestInfo, FocusEntry replacedCurrentEntry)483     private OemCarAudioFocusResult evaluateFocusRequestExternallyLocked(AudioFocusInfo requestInfo,
484             FocusEntry replacedCurrentEntry) {
485         OemCarAudioFocusEvaluationRequest request =
486                 new OemCarAudioFocusEvaluationRequest.Builder(getMutedVolumeGroups(),
487                         getAudioFocusEntries(mFocusHolders, replacedCurrentEntry),
488                         getAudioFocusEntries(mFocusLosers, replacedCurrentEntry),
489                         mAudioZoneId).setAudioFocusRequest(convertAudioFocusInfo(requestInfo))
490                         .build();
491 
492         logFocusEvent("Calling oem service with request " + request);
493         return CarLocalServices.getService(CarOemProxyService.class)
494                 .getCarOemAudioFocusService().evaluateAudioFocusRequest(request);
495     }
496 
convertAudioFocusInfo(AudioFocusInfo info)497     private AudioFocusEntry convertAudioFocusInfo(AudioFocusInfo info) {
498         return new AudioFocusEntry.Builder(info,
499                 mCarAudioContext.getContextForAudioAttribute(info.getAttributes()),
500                 getVolumeGroupForAttribute(info.getAttributes()),
501                 AUDIOFOCUS_LOSS_TRANSIENT).build();
502     }
503 
getAudioFocusEntries(ArrayMap<String, FocusEntry> entryMap, FocusEntry replacedCurrentEntry)504     private List<AudioFocusEntry> getAudioFocusEntries(ArrayMap<String, FocusEntry> entryMap,
505             FocusEntry replacedCurrentEntry) {
506         List<AudioFocusEntry> entries = new ArrayList<>(entryMap.size());
507         for (int index = 0; index < entryMap.size(); index++) {
508             // Will consider focus evaluation for a current entry and replace it if focus is
509             // granted
510             if (replacedCurrentEntry != null
511                     &&  replacedCurrentEntry.getClientId().equals(entryMap.keyAt(index))) {
512                 continue;
513             }
514             entries.add(convertFocusEntry(entryMap.valueAt(index)));
515         }
516         return entries;
517     }
518 
convertFocusEntry(FocusEntry entry)519     private AudioFocusEntry convertFocusEntry(FocusEntry entry) {
520         return convertAudioFocusInfo(entry.getAudioFocusInfo());
521     }
522 
getMutedVolumeGroups()523     private List<CarVolumeGroupInfo> getMutedVolumeGroups() {
524         return mCarVolumeInfoWrapper.getMutedVolumeGroups(mAudioZoneId);
525     }
526 
isExternalFocusEnabled()527     private boolean isExternalFocusEnabled() {
528         CarOemProxyService proxy = CarLocalServices.getService(CarOemProxyService.class);
529         if (proxy == null || !proxy.isOemServiceEnabled()) {
530             return false;
531         }
532 
533         if (!proxy.isOemServiceReady()) {
534             logFocusEvent("Focus was called but OEM service is not yet ready.");
535             return false;
536         }
537 
538         return proxy.getCarOemAudioFocusService() != null;
539     }
540 
convertAudioFocusEntries(List<FocusEntry> changedEntries)541     private List<AudioFocusEntry> convertAudioFocusEntries(List<FocusEntry> changedEntries) {
542         List<AudioFocusEntry> audioFocusEntries = new ArrayList<>(changedEntries.size());
543         for (int index = 0; index < changedEntries.size(); index++) {
544             audioFocusEntries.add(convertFocusEntry(changedEntries.get(index)));
545         }
546         return audioFocusEntries;
547     }
548 
getVolumeGroupForAttribute(AudioAttributes attributes)549     private int getVolumeGroupForAttribute(AudioAttributes attributes) {
550         return mCarVolumeInfoWrapper.getVolumeGroupIdForAudioAttribute(mAudioZoneId, attributes);
551     }
552 
553     @GuardedBy("mLock")
evaluateAgainstFocusLosersLocked( FocusEntry replacedBlockedEntry, int requestedContext, boolean allowDucking, boolean allowDelayedFocus)554     private FocusEvaluation evaluateAgainstFocusLosersLocked(
555             FocusEntry replacedBlockedEntry, int requestedContext, boolean allowDucking,
556             boolean allowDelayedFocus) {
557         Slogf.i(TAG, "Scanning those who've already lost focus...");
558         return evaluateAgainstFocusArrayLocked(mFocusLosers, replacedBlockedEntry,
559                 requestedContext, allowDucking, allowDelayedFocus);
560     }
561 
562     @GuardedBy("mLock")
evaluateAgainstFocusHoldersLocked( FocusEntry replacedCurrentEntry, int requestedContext, boolean allowDucking, boolean allowDelayedFocus)563     private FocusEvaluation evaluateAgainstFocusHoldersLocked(
564             FocusEntry replacedCurrentEntry, int requestedContext, boolean allowDucking,
565             boolean allowDelayedFocus) {
566         Slogf.i(TAG, "Scanning focus holders...");
567         return evaluateAgainstFocusArrayLocked(mFocusHolders, replacedCurrentEntry,
568                 requestedContext, allowDucking, allowDelayedFocus);
569     }
570 
571     @GuardedBy("mLock")
evaluateAgainstFocusArrayLocked( ArrayMap<String, FocusEntry> focusArray, FocusEntry replacedEntry, int requestedContext, boolean allowDucking, boolean allowDelayedFocus)572     private FocusEvaluation evaluateAgainstFocusArrayLocked(
573             ArrayMap<String, FocusEntry> focusArray,
574             FocusEntry replacedEntry, int requestedContext, boolean allowDucking,
575             boolean allowDelayedFocus) {
576         boolean delayFocusForCurrentRequest = false;
577         ArrayList<FocusEntry> changedEntries = new ArrayList<FocusEntry>();
578         for (int index = 0; index < focusArray.size(); index++) {
579             FocusEntry entry = focusArray.valueAt(index);
580             Slogf.i(TAG, entry.getAudioFocusInfo().getClientId());
581 
582             if (replacedEntry != null && entry.getClientId().equals(replacedEntry.getClientId())) {
583                 continue;
584             }
585 
586             int interactionResult = mFocusInteraction
587                     .evaluateRequest(requestedContext, entry, changedEntries, allowDucking,
588                             allowDelayedFocus);
589             if (interactionResult == AUDIOFOCUS_REQUEST_FAILED) {
590                 return FocusEvaluation.FOCUS_EVALUATION_FAILED;
591             }
592             if (interactionResult == AUDIOFOCUS_REQUEST_DELAYED) {
593                 delayFocusForCurrentRequest = true;
594             }
595         }
596         int results = delayFocusForCurrentRequest
597                 ? AUDIOFOCUS_REQUEST_DELAYED : AUDIOFOCUS_REQUEST_GRANTED;
598         return new FocusEvaluation(changedEntries, results);
599     }
600 
canSwapCallOrRingerClientRequest(String clientId, AudioAttributes currentAttributes, AudioAttributes requestedAttributes)601     private static boolean canSwapCallOrRingerClientRequest(String clientId,
602             AudioAttributes currentAttributes, AudioAttributes requestedAttributes) {
603         return isCallFocusRequestClientId(clientId)
604                 && isRingerOrCallAudioAttributes(currentAttributes)
605                 && isRingerOrCallAudioAttributes(requestedAttributes);
606     }
607 
isRingerOrCallAudioAttributes(AudioAttributes attributes)608     private static boolean isRingerOrCallAudioAttributes(AudioAttributes attributes) {
609         return CarAudioContext.isRingerOrCallAudioAttribute(attributes);
610     }
611 
612     @Override
onAudioFocusRequest(AudioFocusInfo afi, int requestResult)613     public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {
614         int response;
615         AudioPolicy policy;
616         synchronized (mLock) {
617             policy = mAudioPolicy;
618             response = evaluateFocusRequestLocked(afi);
619         }
620 
621         // Post our reply for delivery to the original focus requester
622         mAudioManager.setFocusRequestResult(afi, response, policy);
623         logFocusEvent("onAudioFocusRequest for client " + afi.getClientId()
624                 + " with gain type " + focusEventToString(afi.getGainRequest())
625                 + " resulted in " + focusRequestResponseToString(response));
626     }
627 
swapDelayedAudioFocusRequestLocked(AudioFocusInfo afi)628     private void swapDelayedAudioFocusRequestLocked(AudioFocusInfo afi) {
629         // If we are swapping to a different client then send the focus loss signal
630         if (mDelayedRequest != null
631                 && !afi.getClientId().equals(mDelayedRequest.getClientId())) {
632             sendFocusLossLocked(mDelayedRequest, AUDIOFOCUS_LOSS);
633         }
634         mDelayedRequest = afi;
635     }
636 
canReceiveDelayedFocus(AudioFocusInfo afi)637     private boolean canReceiveDelayedFocus(AudioFocusInfo afi) {
638         if (afi.getGainRequest() != AUDIOFOCUS_GAIN) {
639             return false;
640         }
641         return (afi.getFlags() & AUDIOFOCUS_FLAG_DELAY_OK) == AUDIOFOCUS_FLAG_DELAY_OK;
642     }
643 
644     /**
645      * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes)
646      * Note that we'll get this call for a focus holder that dies while in the focus stack, so
647      * we don't need to watch for death notifications directly.
648      * */
649     @Override
onAudioFocusAbandon(AudioFocusInfo afi)650     public void onAudioFocusAbandon(AudioFocusInfo afi) {
651         logFocusEvent("onAudioFocusAbandon for client " + afi.getClientId());
652         synchronized (mLock) {
653             FocusEntry deadEntry = removeFocusEntryLocked(afi);
654 
655             if (deadEntry != null) {
656                 removeBlockerAndRestoreUnblockedWaitersLocked(deadEntry);
657             } else {
658                 removeDelayedAudioFocusRequestLocked(afi);
659             }
660         }
661     }
662 
removeDelayedAudioFocusRequestLocked(AudioFocusInfo afi)663     private void removeDelayedAudioFocusRequestLocked(AudioFocusInfo afi) {
664         if (mDelayedRequest != null && afi.getClientId().equals(mDelayedRequest.getClientId())) {
665             mDelayedRequest = null;
666         }
667     }
668 
669     /**
670      * Remove Focus entry from focus holder or losers entry lists
671      * @param afi Audio Focus Info to remove
672      * @return Removed Focus Entry
673      */
removeFocusEntryLocked(AudioFocusInfo afi)674     private FocusEntry removeFocusEntryLocked(AudioFocusInfo afi) {
675         Slogf.i(TAG, "removeFocusEntry " + afi.getClientId());
676 
677         // Remove this entry from our active or pending list
678         FocusEntry deadEntry = mFocusHolders.remove(afi.getClientId());
679         if (deadEntry == null) {
680             deadEntry = mFocusLosers.remove(afi.getClientId());
681             if (deadEntry == null) {
682                 // Caller is providing an unrecognzied clientId!?
683                 Slogf.w(TAG, "Audio focus abandoned by unrecognized client id: "
684                         + afi.getClientId());
685                 // This probably means an app double released focused for some reason.  One
686                 // harmless possibility is a race between an app being told it lost focus and the
687                 // app voluntarily abandoning focus.  More likely the app is just sloppy.  :)
688                 // The more nefarious possibility is that the clientId is actually corrupted
689                 // somehow, in which case we might have a real focus entry that we're going to fail
690                 // to remove. If that were to happen, I'd expect either the app to swallow it
691                 // silently, or else take unexpected action (eg: resume playing spontaneously), or
692                 // else to see "Failure to signal ..." gain/loss error messages in the log from
693                 // this module when a focus change tries to take action on a truly zombie entry.
694             }
695         }
696         return deadEntry;
697     }
698 
699     @GuardedBy("mLock")
removeBlockerAndRestoreUnblockedWaitersLocked(FocusEntry deadEntry)700     private void removeBlockerAndRestoreUnblockedWaitersLocked(FocusEntry deadEntry) {
701         attemptToGainFocusForDelayedAudioFocusRequestLocked();
702         removeBlockerAndRestoreUnblockedFocusLosersLocked(deadEntry);
703     }
704 
705     @GuardedBy("mLock")
attemptToGainFocusForDelayedAudioFocusRequestLocked()706     private void attemptToGainFocusForDelayedAudioFocusRequestLocked() {
707         if (mDelayedRequest == null) {
708             return;
709         }
710         // Prevent cleanup of permanent lost to recall attemptToGainFocusForDelayedAudioFocusRequest
711         // Whatever granted / denied / delayed again, no need to restore, mDelayedRequest restored
712         // if delayed again.
713         AudioFocusInfo delayedFocusInfo = mDelayedRequest;
714         mDelayedRequest = null;
715         int delayedFocusRequestResults = evaluateFocusRequestLocked(delayedFocusInfo);
716         if (delayedFocusRequestResults == AUDIOFOCUS_REQUEST_GRANTED) {
717             FocusEntry focusEntry = mFocusHolders.get(delayedFocusInfo.getClientId());
718             if (dispatchFocusGainedLocked(focusEntry.getAudioFocusInfo())
719                     == AUDIOFOCUS_REQUEST_FAILED) {
720                 Slogf.e(TAG, "Failure to signal gain of audio focus gain for "
721                         + "delayed focus clientId " + focusEntry.getClientId());
722                 mFocusHolders.remove(focusEntry.getClientId());
723                 removeBlockerFromBlockedFocusLosersLocked(focusEntry);
724                 sendFocusLossLocked(focusEntry.getAudioFocusInfo(),
725                         AUDIOFOCUS_LOSS);
726                 logFocusEvent("Did not gained delayed audio focus for "
727                         + focusEntry.getClientId());
728             }
729         } else if (delayedFocusRequestResults == AUDIOFOCUS_REQUEST_FAILED) {
730             // Delayed request has permanently be denied
731             logFocusEvent("Delayed audio focus retry failed for " + delayedFocusInfo.getClientId());
732             sendFocusLossLocked(delayedFocusInfo, AUDIOFOCUS_LOSS);
733         } else {
734             assert mDelayedRequest.equals(delayedFocusInfo);
735         }
736     }
737 
738     /**
739      * Removes the dead entry from blocked waiters but does not send focus gain signal
740      */
removeBlockerFromBlockedFocusLosersLocked(FocusEntry deadEntry)741     private void removeBlockerFromBlockedFocusLosersLocked(FocusEntry deadEntry) {
742         // Remove this entry from the blocking list of any pending requests
743         Iterator<FocusEntry> it = mFocusLosers.values().iterator();
744         while (it.hasNext()) {
745             FocusEntry entry = it.next();
746             // Remove the retiring entry from all blocker lists
747             entry.removeBlocker(deadEntry);
748         }
749     }
750 
751     /**
752      * Removes the dead entry from blocked waiters and sends focus gain signal
753      */
removeBlockerAndRestoreUnblockedFocusLosersLocked(FocusEntry deadEntry)754     private void removeBlockerAndRestoreUnblockedFocusLosersLocked(FocusEntry deadEntry) {
755         // Remove this entry from the blocking list of any pending requests
756         Iterator<FocusEntry> it = mFocusLosers.values().iterator();
757         while (it.hasNext()) {
758             FocusEntry entry = it.next();
759 
760             // Remove the retiring entry from all blocker lists
761             entry.removeBlocker(deadEntry);
762 
763             // Any entry whose blocking list becomes empty should regain focus
764             if (entry.isUnblocked()) {
765                 Slogf.i(TAG, "Restoring unblocked entry " + entry.getClientId());
766                 // Pull this entry out of the focus losers list
767                 it.remove();
768 
769                 // Add it back into the focus holders list
770                 mFocusHolders.put(entry.getClientId(), entry);
771 
772                 dispatchFocusGainedLocked(entry.getAudioFocusInfo());
773             }
774         }
775     }
776 
777     /**
778      * Dispatch focus gain
779      * @param afi Audio focus info
780      * @return {@link AUDIOFOCUS_REQUEST_GRANTED} if focus is dispatched successfully
781      */
dispatchFocusGainedLocked(AudioFocusInfo afi)782     private int dispatchFocusGainedLocked(AudioFocusInfo afi) {
783         // Send the focus (re)gain notification
784         int result = mAudioManager.dispatchAudioFocusChange(afi, AUDIOFOCUS_GAIN, mAudioPolicy);
785         if (result != AUDIOFOCUS_REQUEST_GRANTED) {
786             // TODO:  Is this actually an error, or is it okay for an entry in the focus
787             // stack to NOT have a listener?  If that's the case, should we even keep
788             // it in the focus stack?
789             Slogf.e(TAG, "Failure to signal gain of audio focus with error: " + result);
790         }
791 
792         logFocusEvent("dispatchFocusGainedLocked for client " + afi.getClientId()
793                         + " with gain type " + focusEventToString(afi.getGainRequest())
794                         + " resulted in " + focusRequestResponseToString(result));
795         return result;
796     }
797 
798     /**
799      * Query the current list of focus loser for uid
800      * @param uid uid to query current focus loser
801      * @return list of current focus losers for uid
802      */
getAudioFocusLosersForUid(int uid)803     ArrayList<AudioFocusInfo> getAudioFocusLosersForUid(int uid) {
804         return getAudioFocusListForUid(uid, mFocusLosers);
805     }
806 
807     /**
808      * Query the current list of focus holders for uid
809      * @param uid uid to query current focus holders
810      * @return list of current focus holders that for uid
811      */
getAudioFocusHoldersForUid(int uid)812     ArrayList<AudioFocusInfo> getAudioFocusHoldersForUid(int uid) {
813         return getAudioFocusListForUid(uid, mFocusHolders);
814     }
815 
getAudioFocusHolders()816     List<AudioFocusInfo> getAudioFocusHolders() {
817         return getAudioFocusInfos(mFocusHolders);
818     }
819 
getAudioFocusLosers()820     List<AudioFocusInfo> getAudioFocusLosers() {
821         return getAudioFocusInfos(mFocusLosers);
822     }
823 
getAudioFocusInfos(ArrayMap<String, FocusEntry> focusEntries)824     private List<AudioFocusInfo> getAudioFocusInfos(ArrayMap<String, FocusEntry> focusEntries) {
825         synchronized (mLock) {
826             List<AudioFocusInfo> focusInfos = new ArrayList<>(focusEntries.size());
827             for (int index = 0; index < focusEntries.size(); index++) {
828                 focusInfos.add(focusEntries.valueAt(index).getAudioFocusInfo());
829             }
830             return focusInfos;
831         }
832     }
833 
834     /**
835      * Query input list for matching uid
836      * @param uid uid to match in map
837      * @param mapToQuery map to query for uid info
838      * @return list of audio focus info that match uid
839      */
getAudioFocusListForUid(int uid, Map<String, FocusEntry> mapToQuery)840     private ArrayList<AudioFocusInfo> getAudioFocusListForUid(int uid,
841             Map<String, FocusEntry> mapToQuery) {
842         ArrayList<AudioFocusInfo> matchingInfoList = new ArrayList<>();
843         synchronized (mLock) {
844             for (String clientId : mapToQuery.keySet()) {
845                 AudioFocusInfo afi = mapToQuery.get(clientId).getAudioFocusInfo();
846                 if (afi.getClientUid() == uid) {
847                     matchingInfoList.add(afi);
848                 }
849             }
850         }
851         return matchingInfoList;
852     }
853 
854     /**
855      * Remove the audio focus info, if entry is still active
856      * dispatch lose focus transient to listeners
857      * @param afi Audio Focus info to remove
858      */
removeAudioFocusInfoAndTransientlyLoseFocus(AudioFocusInfo afi)859     void removeAudioFocusInfoAndTransientlyLoseFocus(AudioFocusInfo afi) {
860         synchronized (mLock) {
861             FocusEntry deadEntry = removeFocusEntryLocked(afi);
862             if (deadEntry != null) {
863                 sendFocusLossLocked(deadEntry.getAudioFocusInfo(),
864                         AUDIOFOCUS_LOSS_TRANSIENT);
865                 removeBlockerAndRestoreUnblockedWaitersLocked(deadEntry);
866             }
867         }
868     }
869 
870     /**
871      * Reevaluate focus request and regain focus
872      * @param afi audio focus info to reevaluate
873      * @return {@link AUDIOFOCUS_REQUEST_GRANTED} if focus is granted
874      */
reevaluateAndRegainAudioFocus(AudioFocusInfo afi)875     int reevaluateAndRegainAudioFocus(AudioFocusInfo afi) {
876         int results;
877         synchronized (mLock) {
878             results = evaluateFocusRequestLocked(afi);
879             if (results == AUDIOFOCUS_REQUEST_GRANTED) {
880                 results = dispatchFocusGainedLocked(afi);
881             }
882         }
883 
884         return results;
885     }
886 
887     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)888     public void dump(IndentingPrintWriter writer) {
889         synchronized (mLock) {
890             writer.println("*CarAudioFocus*");
891             writer.increaseIndent();
892             writer.printf("Audio Zone ID: %d\n", mAudioZoneId);
893             writer.printf("Is focus restricted? %b\n", mIsFocusRestricted);
894             writer.printf("Is external focus eval enabled? %b\n", isExternalFocusEnabled());
895             writer.println();
896             mFocusInteraction.dump(writer);
897 
898             writer.println("Current Focus Holders:");
899             writer.increaseIndent();
900             for (String clientId : mFocusHolders.keySet()) {
901                 mFocusHolders.get(clientId).dump(writer);
902             }
903             writer.decreaseIndent();
904 
905             writer.println("Transient Focus Losers:");
906             writer.increaseIndent();
907             for (String clientId : mFocusLosers.keySet()) {
908                 mFocusLosers.get(clientId).dump(writer);
909             }
910             writer.decreaseIndent();
911 
912             writer.printf("Queued Delayed Focus: %s\n",
913                     mDelayedRequest == null ? "None" : mDelayedRequest.getClientId());
914 
915             writer.println("Focus Events:");
916             writer.increaseIndent();
917             mFocusEventLogger.dump(writer);
918             writer.decreaseIndent();
919 
920             writer.decreaseIndent();
921         }
922     }
923 
focusEventToString(int focusEvent)924     private static String focusEventToString(int focusEvent) {
925         switch (focusEvent) {
926             case AUDIOFOCUS_GAIN:
927                 return "GAIN";
928             case AUDIOFOCUS_GAIN_TRANSIENT:
929                 return "GAIN_TRANSIENT";
930             case AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
931                 return "GAIN_TRANSIENT_EXCLUSIVE";
932             case AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
933                 return "GAIN_TRANSIENT_MAY_DUCK";
934             case AUDIOFOCUS_LOSS:
935                 return "LOSS";
936             case AUDIOFOCUS_LOSS_TRANSIENT:
937                 return "LOSS_TRANSIENT";
938             case AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
939                 return "LOSS_TRANSIENT_CAN_DUCK";
940             default:
941                 return "unknown event " + focusEvent;
942         }
943     }
944 
focusRequestResponseToString(int response)945     private static String focusRequestResponseToString(int response) {
946         if (response == AUDIOFOCUS_REQUEST_GRANTED) {
947             return "REQUEST_GRANTED";
948         } else if (response == AUDIOFOCUS_REQUEST_FAILED) {
949             return "REQUEST_FAILED";
950         }
951         return "REQUEST_DELAYED";
952     }
953 
logFocusEvent(String log)954     private void logFocusEvent(String log) {
955         mFocusEventLogger.log(log);
956         Slogf.i(TAG, log);
957     }
958 
959     /**
960      * Returns the focus interaction for this car focus instance.
961      */
getFocusInteraction()962     public FocusInteraction getFocusInteraction() {
963         return mFocusInteraction;
964     }
965 
966     private static final class FocusEvaluation {
967 
968         private static final FocusEvaluation FOCUS_EVALUATION_FAILED =
969                 new FocusEvaluation(/* changedEntries= */ new ArrayList<>(/* initialCap= */ 0),
970                         AUDIOFOCUS_REQUEST_FAILED);
971 
972         private final List<FocusEntry> mChangedEntries;
973         private final int mAudioFocusEvalResults;
974 
FocusEvaluation(List<FocusEntry> changedEntries, int audioFocusEvalResults)975         FocusEvaluation(List<FocusEntry> changedEntries, int audioFocusEvalResults) {
976             mChangedEntries = changedEntries;
977             mAudioFocusEvalResults = audioFocusEvalResults;
978         }
979 
980         @Override
toString()981         public String toString() {
982             return new StringBuilder().append("{Changed Entries: ").append(mChangedEntries)
983                     .append(", Results: ").append(mAudioFocusEvalResults)
984                     .append(" }").toString();
985         }
986     }
987 }
988