Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 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 | */ |
Scott Randolph | 12edb60 | 2018-11-28 18:57:09 -0800 | [diff] [blame] | 16 | package com.android.car.audio; |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 17 | |
| 18 | import android.hardware.automotive.audiocontrol.V1_0.ContextNumber; |
| 19 | import android.media.AudioAttributes; |
| 20 | import android.media.AudioFocusInfo; |
| 21 | import android.media.AudioManager; |
| 22 | import android.media.audiopolicy.AudioPolicy; |
| 23 | import android.util.Log; |
| 24 | |
| 25 | import java.io.PrintWriter; |
| 26 | import java.util.ArrayList; |
| 27 | import java.util.HashMap; |
| 28 | import java.util.Iterator; |
| 29 | |
| 30 | |
| 31 | public class CarAudioFocus extends AudioPolicy.AudioPolicyFocusListener { |
| 32 | |
| 33 | private static final String TAG = "CarAudioFocus"; |
| 34 | |
| 35 | private final AudioManager mAudioManager; |
| 36 | private CarAudioService mCarAudioService; // Dynamically assigned just after construction |
| 37 | private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction |
| 38 | |
| 39 | |
| 40 | // Values for the internal interaction matrix we use to make focus decisions |
| 41 | private static final int INTERACTION_REJECT = 0; // Focus not granted |
| 42 | private static final int INTERACTION_EXCLUSIVE = 1; // Focus granted, others loose focus |
| 43 | private static final int INTERACTION_CONCURRENT = 2; // Focus granted, others keep focus |
| 44 | |
| 45 | // TODO: Make this an overlayable resource... |
| 46 | // MUSIC = 1, // Music playback |
| 47 | // NAVIGATION = 2, // Navigation directions |
| 48 | // VOICE_COMMAND = 3, // Voice command session |
| 49 | // CALL_RING = 4, // Voice call ringing |
| 50 | // CALL = 5, // Voice call |
| 51 | // ALARM = 6, // Alarm sound from Android |
| 52 | // NOTIFICATION = 7, // Notifications |
| 53 | // SYSTEM_SOUND = 8, // User interaction sounds (button clicks, etc) |
| 54 | private static int sInteractionMatrix[][] = { |
| 55 | // Row selected by playing sound (labels along the right) |
| 56 | // Column selected by incoming request (labels along the top) |
| 57 | // Cell value is one of INTERACTION_REJECT, INTERACTION_EXCLUSIVE, INTERACTION_CONCURRENT |
| 58 | // Invalid, Music, Nav, Voice, Ring, Call, Alarm, Notification, System |
| 59 | { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid |
| 60 | { 0, 1, 2, 1, 1, 1, 1, 2, 2 }, // Music |
| 61 | { 0, 2, 2, 1, 2, 1, 2, 2, 2 }, // Nav |
| 62 | { 0, 2, 0, 2, 1, 1, 0, 0, 0 }, // Voice |
| 63 | { 0, 0, 2, 2, 2, 2, 0, 0, 2 }, // Ring |
| 64 | { 0, 0, 2, 0, 2, 2, 2, 2, 0 }, // Context |
| 65 | { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // Alarm |
| 66 | { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // Notification |
| 67 | { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // System |
| 68 | }; |
| 69 | |
| 70 | |
| 71 | private class FocusEntry { |
| 72 | // Requester info |
| 73 | final AudioFocusInfo mAfi; // never null |
| 74 | |
| 75 | final int mAudioContext; // Which HAL level context does this affect |
| 76 | final ArrayList<FocusEntry> mBlockers; // List of requests that block ours |
| 77 | |
| 78 | FocusEntry(AudioFocusInfo afi, |
| 79 | int context) { |
| 80 | mAfi = afi; |
| 81 | mAudioContext = context; |
| 82 | mBlockers = new ArrayList<FocusEntry>(); |
| 83 | } |
| 84 | |
| 85 | public String getClientId() { |
| 86 | return mAfi.getClientId(); |
| 87 | } |
| 88 | |
| 89 | public boolean wantsPauseInsteadOfDucking() { |
| 90 | return (mAfi.getFlags() & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0; |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | |
| 95 | // We keep track of all the focus requesters in this map, with their clientId as the key. |
| 96 | // This is used both for focus dispatch and death handling |
| 97 | // Note that the clientId reflects the AudioManager instance and listener object (if any) |
| 98 | // so that one app can have more than one unique clientId by setting up distinct listeners. |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 99 | // Because the listener gets only LOSS/GAIN messages, this is important for an app to do if |
| 100 | // it expects to request focus concurrently for different USAGEs so it knows which USAGE |
| 101 | // gained or lost focus at any given moment. If the SAME listener is used for requests of |
| 102 | // different USAGE while the earlier request is still in the focus stack (whether holding |
| 103 | // focus or pending), the new request will be REJECTED so as to avoid any confusion about |
| 104 | // the meaning of subsequent GAIN/LOSS events (which would continue to apply to the focus |
| 105 | // request that was already active or pending). |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 106 | private HashMap<String, FocusEntry> mFocusHolders = new HashMap<String, FocusEntry>(); |
| 107 | private HashMap<String, FocusEntry> mFocusLosers = new HashMap<String, FocusEntry>(); |
| 108 | |
| 109 | |
| 110 | CarAudioFocus(AudioManager audioManager) { |
| 111 | mAudioManager = audioManager; |
| 112 | } |
| 113 | |
| 114 | |
| 115 | // This has to happen after the construction to avoid a chicken and egg problem when setting up |
| 116 | // the AudioPolicy which must depend on this object. |
| 117 | public void setOwningPolicy(CarAudioService audioService, AudioPolicy parentPolicy) { |
| 118 | mCarAudioService = audioService; |
| 119 | mAudioPolicy = parentPolicy; |
| 120 | } |
| 121 | |
| 122 | |
| 123 | // This sends a focus loss message to the targeted requester. |
| 124 | private void sendFocusLoss(FocusEntry loser, boolean permanent) { |
| 125 | int lossType = (permanent ? AudioManager.AUDIOFOCUS_LOSS : |
| 126 | AudioManager.AUDIOFOCUS_LOSS_TRANSIENT); |
| 127 | Log.i(TAG, "sendFocusLoss to " + loser.getClientId()); |
| 128 | int result = mAudioManager.dispatchAudioFocusChange(loser.mAfi, lossType, mAudioPolicy); |
| 129 | if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| 130 | // TODO: Is this actually an error, or is it okay for an entry in the focus stack |
| 131 | // to NOT have a listener? If that's the case, should we even keep it in the focus |
| 132 | // stack? |
| 133 | Log.e(TAG, "Failure to signal loss of audio focus with error: " + result); |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | |
| 138 | /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */ |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 139 | // Note that we replicate most, but not all of the behaviors of the default MediaFocusControl |
| 140 | // engine as of Android P. |
| 141 | // Besides the interaction matrix which allows concurrent focus for multiple requestors, which |
| 142 | // is the reason for this module, we also treat repeated requests from the same clientId |
| 143 | // slightly differently. |
| 144 | // If a focus request for the same listener (clientId) is received while that listener is |
| 145 | // already in the focus stack, we REJECT it outright unless it is for the same USAGE. |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 146 | // If it is for the same USAGE, we replace the old request with the new one. |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 147 | // The default audio framework's behavior is to remove the previous entry in the stack (no-op |
| 148 | // if the requester is already holding focus). |
| 149 | int evaluateFocusRequest(AudioFocusInfo afi) { |
| 150 | Log.i(TAG, "Evaluating focus request for client " + afi.getClientId()); |
| 151 | |
| 152 | // Is this a request for premanant focus? |
| 153 | // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -- Means Notifications should be denied |
| 154 | // AUDIOFOCUS_GAIN_TRANSIENT -- Means current focus holders should get transient loss |
| 155 | // AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -- Means other can duck (no loss message from us) |
| 156 | // NOTE: We expect that in practice it will be permanent for all media requests and |
| 157 | // transient for everything else, but that isn't currently an enforced requirement. |
| 158 | final boolean permanent = |
| 159 | (afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN); |
| 160 | final boolean allowDucking = |
| 161 | (afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); |
| 162 | |
| 163 | |
| 164 | // Convert from audio attributes "usage" to HAL level "context" |
| 165 | final int requestedContext = mCarAudioService.getContextForUsage( |
| 166 | afi.getAttributes().getUsage()); |
| 167 | |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 168 | // If we happen to find entries that this new request should replace, we'll store them here. |
| 169 | // This happens when a client makes a second AF request on the same listener. |
| 170 | // After we've granted audio focus to our current request, we'll abandon these requests. |
| 171 | FocusEntry replacedCurrentEntry = null; |
| 172 | FocusEntry replacedBlockedEntry = null; |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 173 | |
| 174 | // Scan all active and pending focus requests. If any should cause rejection of |
| 175 | // this new request, then we're done. Keep a list of those against whom we're exclusive |
| 176 | // so we can update the relationships if/when we are sure we won't get rejected. |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 177 | Log.i(TAG, "Scanning focus holders..."); |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 178 | final ArrayList<FocusEntry> losers = new ArrayList<FocusEntry>(); |
| 179 | for (FocusEntry entry : mFocusHolders.values()) { |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 180 | Log.d(TAG, "Evaluating focus holder: " + entry.getClientId()); |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 181 | |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 182 | // If this request is for Notifications and a current focus holder has specified |
| 183 | // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request. |
| 184 | // This matches the hardwired behavior in the default audio policy engine which apps |
| 185 | // might expect (The interaction matrix doesn't have any provision for dealing with |
| 186 | // override flags like this). |
| 187 | if ((requestedContext == ContextNumber.NOTIFICATION) && |
| 188 | (entry.mAfi.getGainRequest() == |
| 189 | AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) { |
| 190 | return AudioManager.AUDIOFOCUS_REQUEST_FAILED; |
| 191 | } |
| 192 | |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 193 | // We don't allow sharing listeners (client IDs) between two concurrent requests |
| 194 | // (because the app would have no way to know to which request a later event applied) |
| 195 | if (afi.getClientId().equals(entry.mAfi.getClientId())) { |
| 196 | if (entry.mAudioContext == requestedContext) { |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 197 | // This is a request from a current focus holder. |
| 198 | // Abandon the previous request (without sending a LOSS notification to it), |
| 199 | // and don't check the interaction matrix for it. |
| 200 | Log.i(TAG, "Replacing accepted request from same client"); |
| 201 | replacedCurrentEntry = entry; |
| 202 | continue; |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 203 | } else { |
| 204 | // Trivially reject a request for a different USAGE |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 205 | Log.e(TAG, "Client " + entry.getClientId() + " has already requested focus " |
| 206 | + "for " + entry.mAfi.getAttributes().usageToString() + " - cannot " |
| 207 | + "request focus for " + afi.getAttributes().usageToString() + " on " |
| 208 | + "same listener."); |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 209 | return AudioManager.AUDIOFOCUS_REQUEST_FAILED; |
| 210 | } |
| 211 | } |
| 212 | |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 213 | // Check the interaction matrix for the relationship between this entry and the request |
| 214 | switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) { |
| 215 | case INTERACTION_REJECT: |
| 216 | // This request is rejected, so nothing further to do |
| 217 | return AudioManager.AUDIOFOCUS_REQUEST_FAILED; |
| 218 | case INTERACTION_EXCLUSIVE: |
| 219 | // The new request will cause this existing entry to lose focus |
| 220 | losers.add(entry); |
| 221 | break; |
| 222 | default: |
| 223 | // If ducking isn't allowed by the focus requestor, then everybody else |
| 224 | // must get a LOSS. |
| 225 | // If a focus holder has set the AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS flag, |
| 226 | // they must get a LOSS message even if ducking would otherwise be allowed. |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 227 | if (!allowDucking || entry.wantsPauseInsteadOfDucking()) { |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 228 | // The new request will cause audio book to lose focus and pause |
| 229 | losers.add(entry); |
| 230 | } |
| 231 | } |
| 232 | } |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 233 | Log.i(TAG, "Scanning those who've already lost focus..."); |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 234 | final ArrayList<FocusEntry> blocked = new ArrayList<FocusEntry>(); |
| 235 | for (FocusEntry entry : mFocusLosers.values()) { |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 236 | Log.i(TAG, entry.mAfi.getClientId()); |
| 237 | |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 238 | // If this request is for Notifications and a pending focus holder has specified |
| 239 | // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request |
| 240 | if ((requestedContext == ContextNumber.NOTIFICATION) && |
| 241 | (entry.mAfi.getGainRequest() == |
| 242 | AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) { |
| 243 | return AudioManager.AUDIOFOCUS_REQUEST_FAILED; |
| 244 | } |
| 245 | |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 246 | // We don't allow sharing listeners (client IDs) between two concurrent requests |
| 247 | // (because the app would have no way to know to which request a later event applied) |
| 248 | if (afi.getClientId().equals(entry.mAfi.getClientId())) { |
| 249 | if (entry.mAudioContext == requestedContext) { |
| 250 | // This is a repeat of a request that is currently blocked. |
| 251 | // Evaluate it as if it were a new request, but note that we should remove |
| 252 | // the old pending request, and move it. |
| 253 | // We do not want to evaluate the new request against itself. |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 254 | Log.i(TAG, "Replacing pending request from same client"); |
| 255 | replacedBlockedEntry = entry; |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 256 | continue; |
| 257 | } else { |
| 258 | // Trivially reject a request for a different USAGE |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 259 | Log.e(TAG, "Client " + entry.getClientId() + " has already requested focus " |
| 260 | + "for " + entry.mAfi.getAttributes().usageToString() + " - cannot " |
| 261 | + "request focus for " + afi.getAttributes().usageToString() + " on " |
| 262 | + "same listener."); |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 263 | return AudioManager.AUDIOFOCUS_REQUEST_FAILED; |
| 264 | } |
| 265 | } |
| 266 | |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 267 | // Check the interaction matrix for the relationship between this entry and the request |
| 268 | switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) { |
| 269 | case INTERACTION_REJECT: |
| 270 | // Even though this entry has currently lost focus, the fact that it is |
| 271 | // waiting to play means we'll reject this new conflicting request. |
| 272 | return AudioManager.AUDIOFOCUS_REQUEST_FAILED; |
| 273 | case INTERACTION_EXCLUSIVE: |
| 274 | // The new request is yet another reason this entry cannot regain focus (yet) |
| 275 | blocked.add(entry); |
| 276 | break; |
| 277 | default: |
| 278 | // If ducking is not allowed by the requester, or the pending focus holder had |
| 279 | // set the AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS flag, |
| 280 | // then the pending holder must stay "lost" until this requester goes away. |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 281 | if (!allowDucking || entry.wantsPauseInsteadOfDucking()) { |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 282 | // The new request is yet another reason this entry cannot regain focus yet |
| 283 | blocked.add(entry); |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | |
| 289 | // Now that we've decided we'll grant focus, construct our new FocusEntry |
| 290 | FocusEntry newEntry = new FocusEntry(afi, requestedContext); |
| 291 | |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 292 | // These entries have permanently lost focus as a result of this request, so they |
| 293 | // should be removed from all blocker lists. |
| 294 | ArrayList<FocusEntry> permanentlyLost = new ArrayList<>(); |
| 295 | |
| 296 | if (replacedCurrentEntry != null) { |
| 297 | mFocusHolders.remove(replacedCurrentEntry.getClientId()); |
| 298 | permanentlyLost.add(replacedCurrentEntry); |
| 299 | } |
| 300 | if (replacedBlockedEntry != null) { |
| 301 | mFocusLosers.remove(replacedBlockedEntry.getClientId()); |
| 302 | permanentlyLost.add(replacedBlockedEntry); |
| 303 | } |
| 304 | |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 305 | |
| 306 | // Now that we're sure we'll accept this request, update any requests which we would |
| 307 | // block but are already out of focus but waiting to come back |
| 308 | for (FocusEntry entry : blocked) { |
| 309 | // If we're out of focus it must be because somebody is blocking us |
| 310 | assert !entry.mBlockers.isEmpty(); |
| 311 | |
| 312 | if (permanent) { |
| 313 | // This entry has now lost focus forever |
| 314 | sendFocusLoss(entry, permanent); |
| 315 | final FocusEntry deadEntry = mFocusLosers.remove(entry.mAfi.getClientId()); |
| 316 | assert deadEntry != null; |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 317 | permanentlyLost.add(entry); |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 318 | } else { |
| 319 | // Note that this new request is yet one more reason we can't (yet) have focus |
| 320 | entry.mBlockers.add(newEntry); |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | // Notify and update any requests which are now losing focus as a result of the new request |
| 325 | for (FocusEntry entry : losers) { |
| 326 | // If we have focus (but are about to loose it), nobody should be blocking us yet |
| 327 | assert entry.mBlockers.isEmpty(); |
| 328 | |
| 329 | sendFocusLoss(entry, permanent); |
| 330 | |
| 331 | // The entry no longer holds focus, so take it out of the holders list |
| 332 | mFocusHolders.remove(entry.mAfi.getClientId()); |
| 333 | |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 334 | if (permanent) { |
| 335 | permanentlyLost.add(entry); |
| 336 | } else { |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 337 | // Add ourselves to the list of requests waiting to get focus back and |
| 338 | // note why we lost focus so we can tell when it's time to get it back |
| 339 | mFocusLosers.put(entry.mAfi.getClientId(), entry); |
| 340 | entry.mBlockers.add(newEntry); |
| 341 | } |
| 342 | } |
| 343 | |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 344 | // Now that all new blockers have been added, clear out any other requests that have been |
| 345 | // permanently lost as a result of this request. Treat them as abandoned - if they're on |
| 346 | // any blocker lists, remove them. If any focus requests become unblocked as a result, |
| 347 | // re-grant them. (This can happen when a GAIN_TRANSIENT_MAY_DUCK request replaces a |
| 348 | // GAIN_TRANSIENT request from the same listener.) |
| 349 | for (FocusEntry entry : permanentlyLost) { |
| 350 | Log.d(TAG, "Cleaning up entry " + entry.getClientId()); |
| 351 | removeFocusEntryAndRestoreUnblockedWaiters(entry); |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 352 | } |
| 353 | |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 354 | // Finally, add the request we're granting to the focus holders' list |
| 355 | mFocusHolders.put(afi.getClientId(), newEntry); |
| 356 | |
Scott Randolph | 8eb4955 | 2018-10-17 00:56:39 -0700 | [diff] [blame] | 357 | Log.i(TAG, "AUDIOFOCUS_REQUEST_GRANTED"); |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 358 | return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; |
| 359 | } |
| 360 | |
| 361 | |
| 362 | @Override |
| 363 | public synchronized void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) { |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 364 | Log.i(TAG, "onAudioFocusRequest " + afi.getClientId()); |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 365 | |
| 366 | int response = evaluateFocusRequest(afi); |
| 367 | |
| 368 | // Post our reply for delivery to the original focus requester |
| 369 | mAudioManager.setFocusRequestResult(afi, response, mAudioPolicy); |
| 370 | } |
| 371 | |
| 372 | |
| 373 | /** |
| 374 | * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes) |
Roberto Perez | 0a15c81 | 2018-10-31 12:53:38 -0700 | [diff] [blame] | 375 | * Note that we'll get this call for a focus holder that dies while in the focus stack, so |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 376 | * we don't need to watch for death notifications directly. |
| 377 | * */ |
| 378 | @Override |
| 379 | public synchronized void onAudioFocusAbandon(AudioFocusInfo afi) { |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 380 | Log.i(TAG, "onAudioFocusAbandon " + afi.getClientId()); |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 381 | |
| 382 | // Remove this entry from our active or pending list |
| 383 | FocusEntry deadEntry = mFocusHolders.remove(afi.getClientId()); |
| 384 | if (deadEntry == null) { |
| 385 | deadEntry = mFocusLosers.remove(afi.getClientId()); |
| 386 | if (deadEntry == null) { |
| 387 | // Caller is providing an unrecognzied clientId!? |
Scott Randolph | dc41061 | 2018-10-18 18:43:26 -0700 | [diff] [blame] | 388 | Log.w(TAG, "Audio focus abandoned by unrecognized client id: " + afi.getClientId()); |
| 389 | // This probably means an app double released focused for some reason. One |
| 390 | // harmless possibility is a race between an app being told it lost focus and the |
| 391 | // app voluntarily abandoning focus. More likely the app is just sloppy. :) |
| 392 | // The more nefarious possibility is that the clientId is actually corrupted |
| 393 | // somehow, in which case we might have a real focus entry that we're going to fail |
| 394 | // to remove. If that were to happen, I'd expect either the app to swallow it |
| 395 | // silently, or else take unexpected action (eg: resume playing spontaneously), or |
| 396 | // else to see "Failure to signal ..." gain/loss error messages in the log from |
| 397 | // this module when a focus change tries to take action on a truly zombie entry. |
Roberto Perez | 0a15c81 | 2018-10-31 12:53:38 -0700 | [diff] [blame] | 398 | return; |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 399 | } |
| 400 | } |
| 401 | |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 402 | removeFocusEntryAndRestoreUnblockedWaiters(deadEntry); |
| 403 | } |
| 404 | |
| 405 | private void removeFocusEntryAndRestoreUnblockedWaiters(FocusEntry deadEntry) { |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 406 | // Remove this entry from the blocking list of any pending requests |
| 407 | Iterator<FocusEntry> it = mFocusLosers.values().iterator(); |
| 408 | while (it.hasNext()) { |
| 409 | FocusEntry entry = it.next(); |
| 410 | |
| 411 | // Remove the retiring entry from all blocker lists |
| 412 | entry.mBlockers.remove(deadEntry); |
| 413 | |
| 414 | // Any entry whose blocking list becomes empty should regain focus |
| 415 | if (entry.mBlockers.isEmpty()) { |
Justin Paupore | b11a807 | 2019-03-12 20:03:25 -0700 | [diff] [blame^] | 416 | Log.i(TAG, "Restoring unblocked entry " + entry.getClientId()); |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 417 | // Pull this entry out of the focus losers list |
| 418 | it.remove(); |
| 419 | |
| 420 | // Add it back into the focus holders list |
| 421 | mFocusHolders.put(entry.getClientId(), entry); |
| 422 | |
| 423 | // Send the focus (re)gain notification |
| 424 | int result = mAudioManager.dispatchAudioFocusChange( |
| 425 | entry.mAfi, |
| 426 | entry.mAfi.getGainRequest(), |
| 427 | mAudioPolicy); |
| 428 | if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| 429 | // TODO: Is this actually an error, or is it okay for an entry in the focus |
| 430 | // stack to NOT have a listener? If that's the case, should we even keep |
| 431 | // it in the focus stack? |
| 432 | Log.e(TAG, "Failure to signal gain of audio focus with error: " + result); |
| 433 | } |
| 434 | } |
| 435 | } |
| 436 | } |
| 437 | |
Scott Randolph | 54892f8 | 2018-03-02 13:24:55 -0800 | [diff] [blame] | 438 | public synchronized void dump(PrintWriter writer) { |
| 439 | writer.println("*CarAudioFocus*"); |
| 440 | |
| 441 | writer.println(" Current Focus Holders:"); |
| 442 | for (String clientId : mFocusHolders.keySet()) { |
| 443 | System.out.println(clientId); |
| 444 | } |
| 445 | |
| 446 | writer.println(" Transient Focus Losers:"); |
| 447 | for (String clientId : mFocusLosers.keySet()) { |
| 448 | System.out.println(clientId); |
| 449 | } |
| 450 | } |
| 451 | } |