1 /* 2 * Copyright (C) 2021 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 17 package com.android.server.audio; 18 19 import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.media.AudioAttributes; 24 import android.media.AudioManager; 25 import android.media.AudioPlaybackConfiguration; 26 import android.media.FadeManagerConfiguration; 27 import android.media.VolumeShaper; 28 import android.util.Slog; 29 import android.util.SparseArray; 30 31 import com.android.internal.annotations.GuardedBy; 32 import com.android.server.utils.EventLogger; 33 34 import java.io.PrintWriter; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.Map; 38 39 /** 40 * Class to handle fading out players 41 */ 42 public final class FadeOutManager { 43 44 public static final String TAG = "AS.FadeOutManager"; 45 46 private static final boolean DEBUG = PlaybackActivityMonitor.DEBUG; 47 48 private final Object mLock = new Object(); 49 50 /** 51 * Map of uid (key) to faded out apps (value) 52 */ 53 @GuardedBy("mLock") 54 private final SparseArray<FadedOutApp> mUidToFadedAppsMap = new SparseArray<>(); 55 56 private final FadeConfigurations mFadeConfigurations = new FadeConfigurations(); 57 58 /** 59 * Sets the custom fade manager configuration to be used for player fade out and in 60 * 61 * @param fadeManagerConfig custom fade manager configuration 62 * @return {@link AudioManager#SUCCESS} if setting fade manager config succeeded, 63 * {@link AudioManager#ERROR} otherwise 64 */ setFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig)65 int setFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) { 66 // locked to ensure the fade configs are not updated while faded app state is being updated 67 synchronized (mLock) { 68 return mFadeConfigurations.setFadeManagerConfiguration(fadeManagerConfig); 69 } 70 } 71 72 /** 73 * Clears the fade manager configuration that was previously set with 74 * {@link #setFadeManagerConfiguration(FadeManagerConfiguration)} 75 * 76 * @return {@link AudioManager#SUCCESS} if clearing fade manager config succeeded, 77 * {@link AudioManager#ERROR} otherwise 78 */ clearFadeManagerConfiguration()79 int clearFadeManagerConfiguration() { 80 // locked to ensure the fade configs are not updated while faded app state is being updated 81 synchronized (mLock) { 82 return mFadeConfigurations.clearFadeManagerConfiguration(); 83 } 84 } 85 86 /** 87 * Returns the active fade manager configuration 88 * 89 * @return the {@link FadeManagerConfiguration} 90 */ getFadeManagerConfiguration()91 FadeManagerConfiguration getFadeManagerConfiguration() { 92 return mFadeConfigurations.getFadeManagerConfiguration(); 93 } 94 95 /** 96 * Sets the transient fade manager configuration to be used for player fade out and in 97 * 98 * @param fadeManagerConfig fade manager config that has higher priority than the existing 99 * fade manager configuration. This is expected to be transient. 100 * @return {@link AudioManager#SUCCESS} if setting fade manager config succeeded, 101 * {@link AudioManager#ERROR} otherwise 102 */ setTransientFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig)103 int setTransientFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) { 104 // locked to ensure the fade configs are not updated while faded app state is being updated 105 synchronized (mLock) { 106 return mFadeConfigurations.setTransientFadeManagerConfiguration(fadeManagerConfig); 107 } 108 } 109 110 /** 111 * Clears the transient fade manager configuration that was previously set with 112 * {@link #setTransientFadeManagerConfiguration(FadeManagerConfiguration)} 113 * 114 * @return {@link AudioManager#SUCCESS} if clearing fade manager config succeeded, 115 * {@link AudioManager#ERROR} otherwise 116 */ clearTransientFadeManagerConfiguration()117 int clearTransientFadeManagerConfiguration() { 118 // locked to ensure the fade configs are not updated while faded app state is being updated 119 synchronized (mLock) { 120 return mFadeConfigurations.clearTransientFadeManagerConfiguration(); 121 } 122 } 123 124 /** 125 * Query if fade is enblead and can be enforced on players 126 * 127 * @return {@code true} if fade is enabled, {@code false} otherwise. 128 */ isFadeEnabled()129 boolean isFadeEnabled() { 130 return mFadeConfigurations.isFadeEnabled(); 131 } 132 133 // TODO explore whether a shorter fade out would be a better UX instead of not fading out at all 134 // (legacy behavior) 135 /** 136 * Determine whether the focus request would trigger a fade out, given the parameters of the 137 * requester and those of the focus loser 138 * @param requester the parameters for the focus request 139 * @return {@code true} if there can be a fade out over the requester starting to play 140 */ canCauseFadeOut(@onNull FocusRequester requester, @NonNull FocusRequester loser)141 boolean canCauseFadeOut(@NonNull FocusRequester requester, @NonNull FocusRequester loser) { 142 if (requester.getAudioAttributes().getContentType() == AudioAttributes.CONTENT_TYPE_SPEECH) 143 { 144 if (DEBUG) { 145 Slog.i(TAG, "not fading out: new focus is for speech"); 146 } 147 return false; 148 } 149 if ((loser.getGrantFlags() & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0) { 150 if (DEBUG) { 151 Slog.i(TAG, "not fading out: loser has PAUSES_ON_DUCKABLE_LOSS"); 152 } 153 return false; 154 } 155 return true; 156 } 157 158 /** 159 * Evaluates whether the player associated with this configuration can and should be faded out 160 * @param apc the configuration of the player 161 * @return {@code true} if player type and AudioAttributes are compatible with fade out 162 */ canBeFadedOut(@onNull AudioPlaybackConfiguration apc)163 boolean canBeFadedOut(@NonNull AudioPlaybackConfiguration apc) { 164 synchronized (mLock) { 165 return mFadeConfigurations.isFadeable(apc.getAudioAttributes(), apc.getClientUid(), 166 apc.getPlayerType()); 167 } 168 } 169 170 /** 171 * Get the duration to fade-out after losing audio focus 172 * @param aa The {@link android.media.AudioAttributes} of the player 173 * @return duration in milliseconds 174 */ getFadeOutDurationOnFocusLossMillis(@onNull AudioAttributes aa)175 long getFadeOutDurationOnFocusLossMillis(@NonNull AudioAttributes aa) { 176 synchronized (mLock) { 177 return mFadeConfigurations.getFadeOutDuration(aa); 178 } 179 } 180 181 /** 182 * Get the delay to fade-in the offending players that do not stop after losing audio focus 183 * @param aa The {@link android.media.AudioAttributes} 184 * @return duration in milliseconds 185 */ getFadeInDelayForOffendersMillis(@onNull AudioAttributes aa)186 long getFadeInDelayForOffendersMillis(@NonNull AudioAttributes aa) { 187 synchronized (mLock) { 188 return mFadeConfigurations.getDelayFadeInOffenders(aa); 189 } 190 } 191 fadeOutUid(int uid, List<AudioPlaybackConfiguration> players)192 void fadeOutUid(int uid, List<AudioPlaybackConfiguration> players) { 193 Slog.i(TAG, "fadeOutUid() uid:" + uid); 194 synchronized (mLock) { 195 if (!mUidToFadedAppsMap.contains(uid)) { 196 mUidToFadedAppsMap.put(uid, new FadedOutApp(uid)); 197 } 198 final FadedOutApp fa = mUidToFadedAppsMap.get(uid); 199 for (AudioPlaybackConfiguration apc : players) { 200 final VolumeShaper.Configuration volShaper = 201 mFadeConfigurations.getFadeOutVolumeShaperConfig(apc.getAudioAttributes()); 202 fa.addFade(apc, /* skipRamp= */ false, volShaper); 203 } 204 } 205 } 206 207 /** 208 * Remove the app for the given UID from the list of faded out apps, unfade out its players 209 * @param uid the uid for the app to unfade out 210 * @param players map of current available players (so we can get an APC from piid) 211 */ unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players)212 void unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players) { 213 Slog.i(TAG, "unfadeOutUid() uid:" + uid); 214 synchronized (mLock) { 215 FadedOutApp fa = mUidToFadedAppsMap.get(uid); 216 if (fa == null) { 217 return; 218 } 219 mUidToFadedAppsMap.remove(uid); 220 221 if (!enableFadeManagerConfiguration()) { 222 fa.removeUnfadeAll(players); 223 return; 224 } 225 226 // since fade manager configs may have volume-shaper config per audio attributes, 227 // iterate through each palyer and gather respective configs for fade in 228 ArrayList<AudioPlaybackConfiguration> apcs = new ArrayList<>(players.values()); 229 for (int index = 0; index < apcs.size(); index++) { 230 AudioPlaybackConfiguration apc = apcs.get(index); 231 VolumeShaper.Configuration config = mFadeConfigurations 232 .getFadeInVolumeShaperConfig(apc.getAudioAttributes()); 233 fa.fadeInPlayer(apc, config); 234 } 235 // ideal case all players should be faded in 236 fa.clear(); 237 } 238 } 239 240 // pre-condition: apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED 241 // see {@link PlaybackActivityMonitor#playerEvent} checkFade(@onNull AudioPlaybackConfiguration apc)242 void checkFade(@NonNull AudioPlaybackConfiguration apc) { 243 if (DEBUG) { 244 Slog.v(TAG, "checkFade() player piid:" 245 + apc.getPlayerInterfaceId() + " uid:" + apc.getClientUid()); 246 } 247 248 synchronized (mLock) { 249 final VolumeShaper.Configuration volShaper = 250 mFadeConfigurations.getFadeOutVolumeShaperConfig(apc.getAudioAttributes()); 251 final FadedOutApp fa = mUidToFadedAppsMap.get(apc.getClientUid()); 252 if (fa == null) { 253 return; 254 } 255 fa.addFade(apc, /* skipRamp= */ true, volShaper); 256 } 257 } 258 259 /** 260 * Remove the player from the list of faded out players because it has been released 261 * @param apc the released player 262 */ removeReleased(@onNull AudioPlaybackConfiguration apc)263 void removeReleased(@NonNull AudioPlaybackConfiguration apc) { 264 final int uid = apc.getClientUid(); 265 if (DEBUG) { 266 Slog.v(TAG, "removedReleased() player piid: " 267 + apc.getPlayerInterfaceId() + " uid:" + uid); 268 } 269 synchronized (mLock) { 270 final FadedOutApp fa = mUidToFadedAppsMap.get(uid); 271 if (fa == null) { 272 return; 273 } 274 fa.removeReleased(apc); 275 } 276 } 277 278 /** 279 * Check if uid is currently faded out 280 * @param uid Client id 281 * @return {@code true} if uid is currently faded out. Othwerwise, {@code false}. 282 */ isUidFadedOut(int uid)283 boolean isUidFadedOut(int uid) { 284 synchronized (mLock) { 285 return mUidToFadedAppsMap.contains(uid); 286 } 287 } 288 dump(PrintWriter pw)289 void dump(PrintWriter pw) { 290 synchronized (mLock) { 291 for (int index = 0; index < mUidToFadedAppsMap.size(); index++) { 292 mUidToFadedAppsMap.valueAt(index).dump(pw); 293 } 294 } 295 } 296 297 //========================================================================= 298 /** 299 * Class to group players from a common app, that are faded out. 300 */ 301 private static final class FadedOutApp { 302 private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED = 303 new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY) 304 .createIfNeeded() 305 .build(); 306 307 // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp 308 private static final VolumeShaper.Operation PLAY_SKIP_RAMP = 309 new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build(); 310 311 private final int mUid; 312 // key -> piid; value -> volume shaper config applied 313 private final SparseArray<VolumeShaper.Configuration> mFadedPlayers = new SparseArray<>(); 314 FadedOutApp(int uid)315 FadedOutApp(int uid) { 316 mUid = uid; 317 } 318 dump(PrintWriter pw)319 void dump(PrintWriter pw) { 320 pw.print("\t uid:" + mUid + " piids:"); 321 for (int index = 0; index < mFadedPlayers.size(); index++) { 322 pw.print("piid: " + mFadedPlayers.keyAt(index) + " Volume shaper: " 323 + mFadedPlayers.valueAt(index)); 324 } 325 pw.println(""); 326 } 327 328 /** 329 * Add this player to the list of faded out players and apply the fade 330 * @param apc a config that satisfies 331 * apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED 332 * @param skipRamp {@code true} if the player should be directly into the end of ramp state. 333 * This value would for instance be {@code false} when adding players at the start 334 * of a fade. 335 */ addFade(@onNull AudioPlaybackConfiguration apc, boolean skipRamp, @NonNull VolumeShaper.Configuration volShaper)336 void addFade(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp, 337 @NonNull VolumeShaper.Configuration volShaper) { 338 final int piid = Integer.valueOf(apc.getPlayerInterfaceId()); 339 340 // positive index return implies player is already faded 341 if (mFadedPlayers.indexOfKey(piid) >= 0) { 342 if (DEBUG) { 343 Slog.v(TAG, "player piid:" + piid + " already faded out"); 344 } 345 return; 346 } 347 if (apc.getPlayerProxy() != null) { 348 applyVolumeShaperInternal(apc, piid, volShaper, 349 skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED, skipRamp, 350 PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT); 351 mFadedPlayers.put(piid, volShaper); 352 } else { 353 if (DEBUG) { 354 Slog.v(TAG, "Error fading out player piid:" + piid 355 + ", player not found for uid " + mUid); 356 } 357 } 358 } 359 removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players)360 void removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players) { 361 for (int index = 0; index < mFadedPlayers.size(); index++) { 362 int piid = mFadedPlayers.keyAt(index); 363 final AudioPlaybackConfiguration apc = players.get(piid); 364 if ((apc != null) && (apc.getPlayerProxy() != null)) { 365 applyVolumeShaperInternal(apc, piid, /* volShaperConfig= */ null, 366 VolumeShaper.Operation.REVERSE, /* skipRamp= */ false, 367 PlaybackActivityMonitor.EVENT_TYPE_FADE_IN); 368 } else { 369 // this piid was in the list of faded players, but wasn't found 370 if (DEBUG) { 371 Slog.v(TAG, "Error unfading out player piid:" + piid 372 + ", player not found for uid " + mUid); 373 } 374 } 375 } 376 mFadedPlayers.clear(); 377 } 378 379 @GuardedBy("mLock") fadeInPlayer(@onNull AudioPlaybackConfiguration apc, @Nullable VolumeShaper.Configuration config)380 void fadeInPlayer(@NonNull AudioPlaybackConfiguration apc, 381 @Nullable VolumeShaper.Configuration config) { 382 int piid = Integer.valueOf(apc.getPlayerInterfaceId()); 383 // if not found, no need to fade in since it was never faded out 384 if (!mFadedPlayers.contains(piid)) { 385 if (DEBUG) { 386 Slog.v(TAG, "Player (piid: " + piid + ") for uid (" + mUid 387 + ") is not faded out, no need to fade in"); 388 } 389 return; 390 } 391 392 VolumeShaper.Operation operation = VolumeShaper.Operation.REVERSE; 393 if (config != null) { 394 // replace and join the volumeshapers with (possibly) in progress fade out operation 395 // for a smoother fade in 396 operation = new VolumeShaper.Operation.Builder() 397 .replace(mFadedPlayers.get(piid).getId(), /* join= */ true).build(); 398 } 399 mFadedPlayers.remove(piid); 400 if (apc.getPlayerProxy() != null) { 401 applyVolumeShaperInternal(apc, piid, config, operation, /* skipRamp= */ false, 402 PlaybackActivityMonitor.EVENT_TYPE_FADE_IN); 403 } else { 404 if (DEBUG) { 405 Slog.v(TAG, "Error fading in player piid:" + piid 406 + ", player not found for uid " + mUid); 407 } 408 } 409 } 410 411 @GuardedBy("mLock") clear()412 void clear() { 413 if (mFadedPlayers.size() > 0) { 414 if (DEBUG) { 415 Slog.v(TAG, "Non empty faded players list being cleared! Faded out players:" 416 + mFadedPlayers); 417 } 418 } 419 // should the players be faded in irrespective? 420 mFadedPlayers.clear(); 421 } 422 removeReleased(@onNull AudioPlaybackConfiguration apc)423 void removeReleased(@NonNull AudioPlaybackConfiguration apc) { 424 mFadedPlayers.delete(Integer.valueOf(apc.getPlayerInterfaceId())); 425 } 426 applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid, VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation, boolean skipRamp, String eventType)427 private void applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid, 428 VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation, 429 boolean skipRamp, String eventType) { 430 VolumeShaper.Configuration config = volShaperConfig; 431 // when operation is reverse, use the fade out volume shaper config instead 432 if (operation.equals(VolumeShaper.Operation.REVERSE)) { 433 config = mFadedPlayers.get(piid); 434 } 435 try { 436 logFadeEvent(apc, piid, volShaperConfig, operation, skipRamp, eventType); 437 apc.getPlayerProxy().applyVolumeShaper(config, operation); 438 } catch (Exception e) { 439 Slog.e(TAG, "Error " + eventType + " piid:" + piid + " uid:" + mUid, e); 440 } 441 } 442 logFadeEvent(AudioPlaybackConfiguration apc, int piid, VolumeShaper.Configuration config, VolumeShaper.Operation operation, boolean skipRamp, String eventType)443 private void logFadeEvent(AudioPlaybackConfiguration apc, int piid, 444 VolumeShaper.Configuration config, VolumeShaper.Operation operation, 445 boolean skipRamp, String eventType) { 446 if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT)) { 447 PlaybackActivityMonitor.sEventLogger.enqueue( 448 (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp, config, operation)) 449 .printLog(TAG)); 450 return; 451 } 452 453 if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_IN)) { 454 PlaybackActivityMonitor.sEventLogger.enqueue( 455 (new PlaybackActivityMonitor.FadeInEvent(apc, skipRamp, config, operation)) 456 .printLog(TAG)); 457 return; 458 } 459 460 PlaybackActivityMonitor.sEventLogger.enqueue( 461 (new EventLogger.StringEvent(eventType + " piid:" + piid)).printLog(TAG)); 462 } 463 } 464 } 465