• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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