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 android.annotation.NonNull; 20 import android.media.AudioAttributes; 21 import android.media.AudioManager; 22 import android.media.AudioPlaybackConfiguration; 23 import android.media.VolumeShaper; 24 import android.util.Log; 25 26 import com.android.internal.util.ArrayUtils; 27 import com.android.server.utils.EventLogger; 28 29 import java.io.PrintWriter; 30 import java.util.ArrayList; 31 import java.util.HashMap; 32 33 /** 34 * Class to handle fading out players 35 */ 36 public final class FadeOutManager { 37 38 public static final String TAG = "AudioService.FadeOutManager"; 39 40 /** duration of the fade out curve */ 41 /*package*/ static final long FADE_OUT_DURATION_MS = 2000; 42 /** 43 * delay after which a faded out player will be faded back in. This will be heard by the user 44 * only in the case of unmuting players that didn't respect audio focus and didn't stop/pause 45 * when their app lost focus. 46 * This is the amount of time between the app being notified of 47 * the focus loss (when its muted by the fade out), and the time fade in (to unmute) starts 48 */ 49 /*package*/ static final long DELAY_FADE_IN_OFFENDERS_MS = 2000; 50 51 private static final boolean DEBUG = PlaybackActivityMonitor.DEBUG; 52 53 private static final VolumeShaper.Configuration FADEOUT_VSHAPE = 54 new VolumeShaper.Configuration.Builder() 55 .setId(PlaybackActivityMonitor.VOLUME_SHAPER_SYSTEM_FADEOUT_ID) 56 .setCurve(new float[]{0.f, 0.25f, 1.0f} /* times */, 57 new float[]{1.f, 0.65f, 0.0f} /* volumes */) 58 .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) 59 .setDuration(FADE_OUT_DURATION_MS) 60 .build(); 61 private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED = 62 new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY) 63 .createIfNeeded() 64 .build(); 65 66 private static final int[] UNFADEABLE_PLAYER_TYPES = { 67 AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO, 68 AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL, 69 }; 70 71 private static final int[] UNFADEABLE_CONTENT_TYPES = { 72 AudioAttributes.CONTENT_TYPE_SPEECH, 73 }; 74 75 private static final int[] FADEABLE_USAGES = { 76 AudioAttributes.USAGE_GAME, 77 AudioAttributes.USAGE_MEDIA, 78 }; 79 80 // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp 81 private static final VolumeShaper.Operation PLAY_SKIP_RAMP = 82 new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build(); 83 84 85 // TODO explore whether a shorter fade out would be a better UX instead of not fading out at all 86 // (legacy behavior) 87 /** 88 * Determine whether the focus request would trigger a fade out, given the parameters of the 89 * requester and those of the focus loser 90 * @param requester the parameters for the focus request 91 * @return true if there can be a fade out over the requester starting to play 92 */ canCauseFadeOut(@onNull FocusRequester requester, @NonNull FocusRequester loser)93 static boolean canCauseFadeOut(@NonNull FocusRequester requester, 94 @NonNull FocusRequester loser) { 95 if (requester.getAudioAttributes().getContentType() == AudioAttributes.CONTENT_TYPE_SPEECH) 96 { 97 if (DEBUG) { Log.i(TAG, "not fading out: new focus is for speech"); } 98 return false; 99 } 100 if ((loser.getGrantFlags() & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0) { 101 if (DEBUG) { Log.i(TAG, "not fading out: loser has PAUSES_ON_DUCKABLE_LOSS"); } 102 return false; 103 } 104 105 return true; 106 } 107 108 /** 109 * Evaluates whether the player associated with this configuration can and should be faded out 110 * @param apc the configuration of the player 111 * @return true if player type and AudioAttributes are compatible with fade out 112 */ canBeFadedOut(@onNull AudioPlaybackConfiguration apc)113 static boolean canBeFadedOut(@NonNull AudioPlaybackConfiguration apc) { 114 if (ArrayUtils.contains(UNFADEABLE_PLAYER_TYPES, apc.getPlayerType())) { 115 if (DEBUG) { Log.i(TAG, "not fading: player type:" + apc.getPlayerType()); } 116 return false; 117 } 118 if (ArrayUtils.contains(UNFADEABLE_CONTENT_TYPES, 119 apc.getAudioAttributes().getContentType())) { 120 if (DEBUG) { 121 Log.i(TAG, "not fading: content type:" 122 + apc.getAudioAttributes().getContentType()); 123 } 124 return false; 125 } 126 if (!ArrayUtils.contains(FADEABLE_USAGES, apc.getAudioAttributes().getUsage())) { 127 if (DEBUG) { 128 Log.i(TAG, "not fading: usage:" + apc.getAudioAttributes().getUsage()); 129 } 130 return false; 131 } 132 return true; 133 } 134 getFadeOutDurationOnFocusLossMillis(AudioAttributes aa)135 static long getFadeOutDurationOnFocusLossMillis(AudioAttributes aa) { 136 if (ArrayUtils.contains(UNFADEABLE_CONTENT_TYPES, aa.getContentType())) { 137 return 0; 138 } 139 if (!ArrayUtils.contains(FADEABLE_USAGES, aa.getUsage())) { 140 return 0; 141 } 142 return FADE_OUT_DURATION_MS; 143 } 144 145 /** 146 * Map of uid (key) to faded out apps (value) 147 */ 148 private final HashMap<Integer, FadedOutApp> mFadedApps = new HashMap<Integer, FadedOutApp>(); 149 fadeOutUid(int uid, ArrayList<AudioPlaybackConfiguration> players)150 synchronized void fadeOutUid(int uid, ArrayList<AudioPlaybackConfiguration> players) { 151 Log.i(TAG, "fadeOutUid() uid:" + uid); 152 if (!mFadedApps.containsKey(uid)) { 153 mFadedApps.put(uid, new FadedOutApp(uid)); 154 } 155 final FadedOutApp fa = mFadedApps.get(uid); 156 for (AudioPlaybackConfiguration apc : players) { 157 fa.addFade(apc, false /*skipRamp*/); 158 } 159 } 160 161 /** 162 * Remove the app for the given UID from the list of faded out apps, unfade out its players 163 * @param uid the uid for the app to unfade out 164 * @param players map of current available players (so we can get an APC from piid) 165 */ unfadeOutUid(int uid, HashMap<Integer, AudioPlaybackConfiguration> players)166 synchronized void unfadeOutUid(int uid, HashMap<Integer, AudioPlaybackConfiguration> players) { 167 Log.i(TAG, "unfadeOutUid() uid:" + uid); 168 final FadedOutApp fa = mFadedApps.remove(uid); 169 if (fa == null) { 170 return; 171 } 172 fa.removeUnfadeAll(players); 173 } 174 175 // pre-condition: apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED 176 // see {@link PlaybackActivityMonitor#playerEvent} checkFade(@onNull AudioPlaybackConfiguration apc)177 synchronized void checkFade(@NonNull AudioPlaybackConfiguration apc) { 178 if (DEBUG) { 179 Log.v(TAG, "checkFade() player piid:" 180 + apc.getPlayerInterfaceId() + " uid:" + apc.getClientUid()); 181 } 182 final FadedOutApp fa = mFadedApps.get(apc.getClientUid()); 183 if (fa == null) { 184 return; 185 } 186 fa.addFade(apc, true); 187 } 188 189 /** 190 * Remove the player from the list of faded out players because it has been released 191 * @param apc the released player 192 */ removeReleased(@onNull AudioPlaybackConfiguration apc)193 synchronized void removeReleased(@NonNull AudioPlaybackConfiguration apc) { 194 final int uid = apc.getClientUid(); 195 if (DEBUG) { 196 Log.v(TAG, "removedReleased() player piid: " 197 + apc.getPlayerInterfaceId() + " uid:" + uid); 198 } 199 final FadedOutApp fa = mFadedApps.get(uid); 200 if (fa == null) { 201 return; 202 } 203 fa.removeReleased(apc); 204 } 205 dump(PrintWriter pw)206 synchronized void dump(PrintWriter pw) { 207 for (FadedOutApp da : mFadedApps.values()) { 208 da.dump(pw); 209 } 210 } 211 212 //========================================================================= 213 /** 214 * Class to group players from a common app, that are faded out. 215 */ 216 private static final class FadedOutApp { 217 private final int mUid; 218 private final ArrayList<Integer> mFadedPlayers = new ArrayList<Integer>(); 219 FadedOutApp(int uid)220 FadedOutApp(int uid) { 221 mUid = uid; 222 } 223 dump(PrintWriter pw)224 void dump(PrintWriter pw) { 225 pw.print("\t uid:" + mUid + " piids:"); 226 for (int piid : mFadedPlayers) { 227 pw.print(" " + piid); 228 } 229 pw.println(""); 230 } 231 232 /** 233 * Add this player to the list of faded out players and apply the fade 234 * @param apc a config that satisfies 235 * apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED 236 * @param skipRamp true if the player should be directly into the end of ramp state. 237 * This value would for instance be false when adding players at the start of a fade. 238 */ addFade(@onNull AudioPlaybackConfiguration apc, boolean skipRamp)239 void addFade(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) { 240 final int piid = new Integer(apc.getPlayerInterfaceId()); 241 if (mFadedPlayers.contains(piid)) { 242 if (DEBUG) { 243 Log.v(TAG, "player piid:" + piid + " already faded out"); 244 } 245 return; 246 } 247 try { 248 PlaybackActivityMonitor.sEventLogger.enqueue( 249 (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp)).printLog(TAG)); 250 apc.getPlayerProxy().applyVolumeShaper( 251 FADEOUT_VSHAPE, 252 skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED); 253 mFadedPlayers.add(piid); 254 } catch (Exception e) { 255 Log.e(TAG, "Error fading out player piid:" + piid 256 + " uid:" + apc.getClientUid(), e); 257 } 258 } 259 removeUnfadeAll(HashMap<Integer, AudioPlaybackConfiguration> players)260 void removeUnfadeAll(HashMap<Integer, AudioPlaybackConfiguration> players) { 261 for (int piid : mFadedPlayers) { 262 final AudioPlaybackConfiguration apc = players.get(piid); 263 if (apc != null) { 264 try { 265 PlaybackActivityMonitor.sEventLogger.enqueue( 266 (new EventLogger.StringEvent("unfading out piid:" 267 + piid)).printLog(TAG)); 268 apc.getPlayerProxy().applyVolumeShaper( 269 FADEOUT_VSHAPE, 270 VolumeShaper.Operation.REVERSE); 271 } catch (Exception e) { 272 Log.e(TAG, "Error unfading out player piid:" + piid + " uid:" + mUid, e); 273 } 274 } else { 275 // this piid was in the list of faded players, but wasn't found 276 if (DEBUG) { 277 Log.v(TAG, "Error unfading out player piid:" + piid 278 + ", player not found for uid " + mUid); 279 } 280 } 281 } 282 mFadedPlayers.clear(); 283 } 284 removeReleased(@onNull AudioPlaybackConfiguration apc)285 void removeReleased(@NonNull AudioPlaybackConfiguration apc) { 286 mFadedPlayers.remove(new Integer(apc.getPlayerInterfaceId())); 287 } 288 } 289 } 290