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