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