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