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