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 if (volShaper != null) { 203 fa.addFade(apc, /* skipRamp= */ false, volShaper); 204 } 205 } 206 } 207 } 208 209 /** 210 * Remove the app for the given UID from the list of faded out apps, unfade out its players 211 * @param uid the uid for the app to unfade out 212 * @param players map of current available players (so we can get an APC from piid) 213 */ unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players)214 void unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players) { 215 Slog.i(TAG, "unfadeOutUid() uid:" + uid); 216 synchronized (mLock) { 217 FadedOutApp fa = mUidToFadedAppsMap.get(uid); 218 if (fa == null) { 219 return; 220 } 221 mUidToFadedAppsMap.remove(uid); 222 223 if (!enableFadeManagerConfiguration()) { 224 fa.removeUnfadeAll(players); 225 return; 226 } 227 228 // since fade manager configs may have volume-shaper config per audio attributes, 229 // iterate through each palyer and gather respective configs for fade in 230 ArrayList<AudioPlaybackConfiguration> apcs = new ArrayList<>(players.values()); 231 for (int index = 0; index < apcs.size(); index++) { 232 AudioPlaybackConfiguration apc = apcs.get(index); 233 VolumeShaper.Configuration config = mFadeConfigurations 234 .getFadeInVolumeShaperConfig(apc.getAudioAttributes()); 235 fa.fadeInPlayer(apc, config); 236 } 237 // ideal case all players should be faded in 238 fa.clear(); 239 } 240 } 241 242 // pre-condition: apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED 243 // see {@link PlaybackActivityMonitor#playerEvent} checkFade(@onNull AudioPlaybackConfiguration apc)244 void checkFade(@NonNull AudioPlaybackConfiguration apc) { 245 if (DEBUG) { 246 Slog.v(TAG, "checkFade() player piid:" 247 + apc.getPlayerInterfaceId() + " uid:" + apc.getClientUid()); 248 } 249 250 synchronized (mLock) { 251 final VolumeShaper.Configuration volShaper = 252 mFadeConfigurations.getFadeOutVolumeShaperConfig(apc.getAudioAttributes()); 253 final FadedOutApp fa = mUidToFadedAppsMap.get(apc.getClientUid()); 254 if (fa == null || volShaper == null) { 255 return; 256 } 257 fa.addFade(apc, /* skipRamp= */ true, volShaper); 258 } 259 } 260 261 /** 262 * Remove the player from the list of faded out players because it has been released 263 * @param apc the released player 264 */ removeReleased(@onNull AudioPlaybackConfiguration apc)265 void removeReleased(@NonNull AudioPlaybackConfiguration apc) { 266 final int uid = apc.getClientUid(); 267 if (DEBUG) { 268 Slog.v(TAG, "removedReleased() player piid: " 269 + apc.getPlayerInterfaceId() + " uid:" + uid); 270 } 271 synchronized (mLock) { 272 final FadedOutApp fa = mUidToFadedAppsMap.get(uid); 273 if (fa == null) { 274 return; 275 } 276 fa.removeReleased(apc); 277 } 278 } 279 280 /** 281 * Check if uid is currently faded out 282 * @param uid Client id 283 * @return {@code true} if uid is currently faded out. Othwerwise, {@code false}. 284 */ isUidFadedOut(int uid)285 boolean isUidFadedOut(int uid) { 286 synchronized (mLock) { 287 return mUidToFadedAppsMap.contains(uid); 288 } 289 } 290 dump(PrintWriter pw)291 void dump(PrintWriter pw) { 292 synchronized (mLock) { 293 for (int index = 0; index < mUidToFadedAppsMap.size(); index++) { 294 mUidToFadedAppsMap.valueAt(index).dump(pw); 295 } 296 } 297 } 298 299 //========================================================================= 300 /** 301 * Class to group players from a common app, that are faded out. 302 */ 303 private static final class FadedOutApp { 304 private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED = 305 new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY) 306 .createIfNeeded() 307 .build(); 308 309 // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp 310 private static final VolumeShaper.Operation PLAY_SKIP_RAMP = 311 new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build(); 312 313 private final int mUid; 314 // key -> piid; value -> volume shaper config applied 315 private final SparseArray<VolumeShaper.Configuration> mFadedPlayers = new SparseArray<>(); 316 FadedOutApp(int uid)317 FadedOutApp(int uid) { 318 mUid = uid; 319 } 320 dump(PrintWriter pw)321 void dump(PrintWriter pw) { 322 pw.print("\t uid:" + mUid + " piids:"); 323 for (int index = 0; index < mFadedPlayers.size(); index++) { 324 pw.print("piid: " + mFadedPlayers.keyAt(index) + " Volume shaper: " 325 + mFadedPlayers.valueAt(index)); 326 } 327 pw.println(""); 328 } 329 330 /** 331 * Add this player to the list of faded out players and apply the fade 332 * @param apc a config that satisfies 333 * apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED 334 * @param skipRamp {@code true} if the player should be directly into the end of ramp state. 335 * This value would for instance be {@code false} when adding players at the start 336 * of a fade. 337 */ addFade(@onNull AudioPlaybackConfiguration apc, boolean skipRamp, @NonNull VolumeShaper.Configuration volShaper)338 void addFade(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp, 339 @NonNull VolumeShaper.Configuration volShaper) { 340 final int piid = Integer.valueOf(apc.getPlayerInterfaceId()); 341 342 // positive index return implies player is already faded 343 if (mFadedPlayers.indexOfKey(piid) >= 0) { 344 if (DEBUG) { 345 Slog.v(TAG, "player piid:" + piid + " already faded out"); 346 } 347 return; 348 } 349 if (apc.getPlayerProxy() != null) { 350 applyVolumeShaperInternal(apc, piid, volShaper, 351 skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED, skipRamp, 352 PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT); 353 mFadedPlayers.put(piid, volShaper); 354 } else { 355 if (DEBUG) { 356 Slog.v(TAG, "Error fading out player piid:" + piid 357 + ", player not found for uid " + mUid); 358 } 359 } 360 } 361 removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players)362 void removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players) { 363 for (int index = 0; index < mFadedPlayers.size(); index++) { 364 int piid = mFadedPlayers.keyAt(index); 365 final AudioPlaybackConfiguration apc = players.get(piid); 366 if ((apc != null) && (apc.getPlayerProxy() != null)) { 367 applyVolumeShaperInternal(apc, piid, /* volShaperConfig= */ null, 368 VolumeShaper.Operation.REVERSE, /* skipRamp= */ false, 369 PlaybackActivityMonitor.EVENT_TYPE_FADE_IN); 370 } else { 371 // this piid was in the list of faded players, but wasn't found 372 if (DEBUG) { 373 Slog.v(TAG, "Error unfading out player piid:" + piid 374 + ", player not found for uid " + mUid); 375 } 376 } 377 } 378 mFadedPlayers.clear(); 379 } 380 381 @GuardedBy("mLock") fadeInPlayer(@onNull AudioPlaybackConfiguration apc, @Nullable VolumeShaper.Configuration config)382 void fadeInPlayer(@NonNull AudioPlaybackConfiguration apc, 383 @Nullable VolumeShaper.Configuration config) { 384 int piid = Integer.valueOf(apc.getPlayerInterfaceId()); 385 // if not found, no need to fade in since it was never faded out 386 if (!mFadedPlayers.contains(piid)) { 387 if (DEBUG) { 388 Slog.v(TAG, "Player (piid: " + piid + ") for uid (" + mUid 389 + ") is not faded out, no need to fade in"); 390 } 391 return; 392 } 393 394 VolumeShaper.Operation operation = VolumeShaper.Operation.REVERSE; 395 if (config != null) { 396 // replace and join the volumeshapers with (possibly) in progress fade out operation 397 // for a smoother fade in 398 operation = new VolumeShaper.Operation.Builder() 399 .replace(mFadedPlayers.get(piid).getId(), /* join= */ true).build(); 400 } 401 mFadedPlayers.remove(piid); 402 if (apc.getPlayerProxy() != null) { 403 applyVolumeShaperInternal(apc, piid, config, operation, /* skipRamp= */ false, 404 PlaybackActivityMonitor.EVENT_TYPE_FADE_IN); 405 } else { 406 if (DEBUG) { 407 Slog.v(TAG, "Error fading in player piid:" + piid 408 + ", player not found for uid " + mUid); 409 } 410 } 411 } 412 413 @GuardedBy("mLock") clear()414 void clear() { 415 if (mFadedPlayers.size() > 0) { 416 if (DEBUG) { 417 Slog.v(TAG, "Non empty faded players list being cleared! Faded out players:" 418 + mFadedPlayers); 419 } 420 } 421 // should the players be faded in irrespective? 422 mFadedPlayers.clear(); 423 } 424 removeReleased(@onNull AudioPlaybackConfiguration apc)425 void removeReleased(@NonNull AudioPlaybackConfiguration apc) { 426 mFadedPlayers.delete(Integer.valueOf(apc.getPlayerInterfaceId())); 427 } 428 applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid, VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation, boolean skipRamp, String eventType)429 private void applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid, 430 VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation, 431 boolean skipRamp, String eventType) { 432 VolumeShaper.Configuration config = volShaperConfig; 433 // when operation is reverse, use the fade out volume shaper config instead 434 if (operation.equals(VolumeShaper.Operation.REVERSE)) { 435 config = mFadedPlayers.get(piid); 436 } 437 try { 438 logFadeEvent(apc, piid, volShaperConfig, operation, skipRamp, eventType); 439 apc.getPlayerProxy().applyVolumeShaper(config, operation); 440 } catch (Exception e) { 441 Slog.e(TAG, "Error " + eventType + " piid:" + piid + " uid:" + mUid, e); 442 } 443 } 444 logFadeEvent(AudioPlaybackConfiguration apc, int piid, VolumeShaper.Configuration config, VolumeShaper.Operation operation, boolean skipRamp, String eventType)445 private void logFadeEvent(AudioPlaybackConfiguration apc, int piid, 446 VolumeShaper.Configuration config, VolumeShaper.Operation operation, 447 boolean skipRamp, String eventType) { 448 if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT)) { 449 PlaybackActivityMonitor.sEventLogger.enqueue( 450 (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp, config, operation)) 451 .printLog(TAG)); 452 return; 453 } 454 455 if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_IN)) { 456 PlaybackActivityMonitor.sEventLogger.enqueue( 457 (new PlaybackActivityMonitor.FadeInEvent(apc, skipRamp, config, operation)) 458 .printLog(TAG)); 459 return; 460 } 461 462 PlaybackActivityMonitor.sEventLogger.enqueue( 463 (new EventLogger.StringEvent(eventType + " piid:" + piid)).printLog(TAG)); 464 } 465 } 466 } 467