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