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 static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.media.AudioAttributes;
24 import android.media.AudioManager;
25 import android.media.AudioPlaybackConfiguration;
26 import android.media.FadeManagerConfiguration;
27 import android.media.VolumeShaper;
28 import android.util.Slog;
29 import android.util.SparseArray;
30 
31 import com.android.internal.annotations.GuardedBy;
32 import com.android.server.utils.EventLogger;
33 
34 import java.io.PrintWriter;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Map;
38 
39 /**
40  * Class to handle fading out players
41  */
42 public final class FadeOutManager {
43 
44     public static final String TAG = "AS.FadeOutManager";
45 
46     private static final boolean DEBUG = PlaybackActivityMonitor.DEBUG;
47 
48     private final Object mLock = new Object();
49 
50     /**
51      * Map of uid (key) to faded out apps (value)
52      */
53     @GuardedBy("mLock")
54     private final SparseArray<FadedOutApp> mUidToFadedAppsMap = new SparseArray<>();
55 
56     private final FadeConfigurations mFadeConfigurations = new FadeConfigurations();
57 
58     /**
59      * Sets the custom fade manager configuration to be used for player fade out and in
60      *
61      * @param fadeManagerConfig custom fade manager configuration
62      * @return {@link AudioManager#SUCCESS} if setting fade manager config succeeded,
63      *     {@link AudioManager#ERROR} otherwise
64      */
setFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig)65     int setFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) {
66         // locked to ensure the fade configs are not updated while faded app state is being updated
67         synchronized (mLock) {
68             return mFadeConfigurations.setFadeManagerConfiguration(fadeManagerConfig);
69         }
70     }
71 
72     /**
73      * Clears the fade manager configuration that was previously set with
74      * {@link #setFadeManagerConfiguration(FadeManagerConfiguration)}
75      *
76      * @return {@link AudioManager#SUCCESS}  if clearing fade manager config succeeded,
77      *     {@link AudioManager#ERROR} otherwise
78      */
clearFadeManagerConfiguration()79     int clearFadeManagerConfiguration() {
80         // locked to ensure the fade configs are not updated while faded app state is being updated
81         synchronized (mLock) {
82             return mFadeConfigurations.clearFadeManagerConfiguration();
83         }
84     }
85 
86     /**
87      * Returns the active fade manager configuration
88      *
89      * @return the {@link FadeManagerConfiguration}
90      */
getFadeManagerConfiguration()91     FadeManagerConfiguration getFadeManagerConfiguration() {
92         return mFadeConfigurations.getFadeManagerConfiguration();
93     }
94 
95     /**
96      * Sets the transient fade manager configuration to be used for player fade out and in
97      *
98      * @param fadeManagerConfig fade manager config that has higher priority than the existing
99      *     fade manager configuration. This is expected to be transient.
100      * @return {@link AudioManager#SUCCESS}  if setting fade manager config succeeded,
101      *     {@link AudioManager#ERROR} otherwise
102      */
setTransientFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig)103     int setTransientFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) {
104         // locked to ensure the fade configs are not updated while faded app state is being updated
105         synchronized (mLock) {
106             return mFadeConfigurations.setTransientFadeManagerConfiguration(fadeManagerConfig);
107         }
108     }
109 
110     /**
111      * Clears the transient fade manager configuration that was previously set with
112      * {@link #setTransientFadeManagerConfiguration(FadeManagerConfiguration)}
113      *
114      * @return {@link AudioManager#SUCCESS}  if clearing fade manager config succeeded,
115      *      {@link AudioManager#ERROR} otherwise
116      */
clearTransientFadeManagerConfiguration()117     int clearTransientFadeManagerConfiguration() {
118         // locked to ensure the fade configs are not updated while faded app state is being updated
119         synchronized (mLock) {
120             return mFadeConfigurations.clearTransientFadeManagerConfiguration();
121         }
122     }
123 
124     /**
125      * Query if fade is enblead and can be enforced on players
126      *
127      * @return {@code true} if fade is enabled, {@code false} otherwise.
128      */
isFadeEnabled()129     boolean isFadeEnabled() {
130         return mFadeConfigurations.isFadeEnabled();
131     }
132 
133     // TODO explore whether a shorter fade out would be a better UX instead of not fading out at all
134     //      (legacy behavior)
135     /**
136      * Determine whether the focus request would trigger a fade out, given the parameters of the
137      * requester and those of the focus loser
138      * @param requester the parameters for the focus request
139      * @return {@code true} if there can be a fade out over the requester starting to play
140      */
canCauseFadeOut(@onNull FocusRequester requester, @NonNull FocusRequester loser)141     boolean canCauseFadeOut(@NonNull FocusRequester requester, @NonNull FocusRequester loser) {
142         if (requester.getAudioAttributes().getContentType() == AudioAttributes.CONTENT_TYPE_SPEECH)
143         {
144             if (DEBUG) {
145                 Slog.i(TAG, "not fading out: new focus is for speech");
146             }
147             return false;
148         }
149         if ((loser.getGrantFlags() & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0) {
150             if (DEBUG) {
151                 Slog.i(TAG, "not fading out: loser has PAUSES_ON_DUCKABLE_LOSS");
152             }
153             return false;
154         }
155         return true;
156     }
157 
158     /**
159      * Evaluates whether the player associated with this configuration can and should be faded out
160      * @param apc the configuration of the player
161      * @return {@code true} if player type and AudioAttributes are compatible with fade out
162      */
canBeFadedOut(@onNull AudioPlaybackConfiguration apc)163     boolean canBeFadedOut(@NonNull AudioPlaybackConfiguration apc) {
164         synchronized (mLock) {
165             return mFadeConfigurations.isFadeable(apc.getAudioAttributes(), apc.getClientUid(),
166                     apc.getPlayerType());
167         }
168     }
169 
170     /**
171      * Get the duration to fade-out after losing audio focus
172      * @param aa The {@link android.media.AudioAttributes} of the player
173      * @return duration in milliseconds
174      */
getFadeOutDurationOnFocusLossMillis(@onNull AudioAttributes aa)175     long getFadeOutDurationOnFocusLossMillis(@NonNull AudioAttributes aa) {
176         synchronized (mLock) {
177             return mFadeConfigurations.getFadeOutDuration(aa);
178         }
179     }
180 
181     /**
182      * Get the delay to fade-in the offending players that do not stop after losing audio focus
183      * @param aa The {@link android.media.AudioAttributes}
184      * @return duration in milliseconds
185      */
getFadeInDelayForOffendersMillis(@onNull AudioAttributes aa)186     long getFadeInDelayForOffendersMillis(@NonNull AudioAttributes aa) {
187         synchronized (mLock) {
188             return mFadeConfigurations.getDelayFadeInOffenders(aa);
189         }
190     }
191 
fadeOutUid(int uid, List<AudioPlaybackConfiguration> players)192     void fadeOutUid(int uid, List<AudioPlaybackConfiguration> players) {
193         Slog.i(TAG, "fadeOutUid() uid:" + uid);
194         synchronized (mLock) {
195             if (!mUidToFadedAppsMap.contains(uid)) {
196                 mUidToFadedAppsMap.put(uid, new FadedOutApp(uid));
197             }
198             final FadedOutApp fa = mUidToFadedAppsMap.get(uid);
199             for (AudioPlaybackConfiguration apc : players) {
200                 final VolumeShaper.Configuration volShaper =
201                         mFadeConfigurations.getFadeOutVolumeShaperConfig(apc.getAudioAttributes());
202                 fa.addFade(apc, /* skipRamp= */ false, volShaper);
203             }
204         }
205     }
206 
207     /**
208      * Remove the app for the given UID from the list of faded out apps, unfade out its players
209      * @param uid the uid for the app to unfade out
210      * @param players map of current available players (so we can get an APC from piid)
211      */
unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players)212     void unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players) {
213         Slog.i(TAG, "unfadeOutUid() uid:" + uid);
214         synchronized (mLock) {
215             FadedOutApp fa = mUidToFadedAppsMap.get(uid);
216             if (fa == null) {
217                 return;
218             }
219             mUidToFadedAppsMap.remove(uid);
220 
221             if (!enableFadeManagerConfiguration()) {
222                 fa.removeUnfadeAll(players);
223                 return;
224             }
225 
226             // since fade manager configs may have volume-shaper config per audio attributes,
227             // iterate through each palyer and gather respective configs  for fade in
228             ArrayList<AudioPlaybackConfiguration> apcs = new ArrayList<>(players.values());
229             for (int index = 0; index < apcs.size(); index++) {
230                 AudioPlaybackConfiguration apc = apcs.get(index);
231                 VolumeShaper.Configuration config = mFadeConfigurations
232                         .getFadeInVolumeShaperConfig(apc.getAudioAttributes());
233                 fa.fadeInPlayer(apc, config);
234             }
235             // ideal case all players should be faded in
236             fa.clear();
237         }
238     }
239 
240     // pre-condition: apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
241     //   see {@link PlaybackActivityMonitor#playerEvent}
checkFade(@onNull AudioPlaybackConfiguration apc)242     void checkFade(@NonNull AudioPlaybackConfiguration apc) {
243         if (DEBUG) {
244             Slog.v(TAG, "checkFade() player piid:"
245                     + apc.getPlayerInterfaceId() + " uid:" + apc.getClientUid());
246         }
247 
248         synchronized (mLock) {
249             final VolumeShaper.Configuration volShaper =
250                     mFadeConfigurations.getFadeOutVolumeShaperConfig(apc.getAudioAttributes());
251             final FadedOutApp fa = mUidToFadedAppsMap.get(apc.getClientUid());
252             if (fa == null) {
253                 return;
254             }
255             fa.addFade(apc, /* skipRamp= */ true, volShaper);
256         }
257     }
258 
259     /**
260      * Remove the player from the list of faded out players because it has been released
261      * @param apc the released player
262      */
removeReleased(@onNull AudioPlaybackConfiguration apc)263     void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
264         final int uid = apc.getClientUid();
265         if (DEBUG) {
266             Slog.v(TAG, "removedReleased() player piid: "
267                     + apc.getPlayerInterfaceId() + " uid:" + uid);
268         }
269         synchronized (mLock) {
270             final FadedOutApp fa = mUidToFadedAppsMap.get(uid);
271             if (fa == null) {
272                 return;
273             }
274             fa.removeReleased(apc);
275         }
276     }
277 
278     /**
279      * Check if uid is currently faded out
280      * @param uid Client id
281      * @return {@code true} if uid is currently faded out. Othwerwise, {@code false}.
282      */
isUidFadedOut(int uid)283     boolean isUidFadedOut(int uid) {
284         synchronized (mLock) {
285             return mUidToFadedAppsMap.contains(uid);
286         }
287     }
288 
dump(PrintWriter pw)289     void dump(PrintWriter pw) {
290         synchronized (mLock) {
291             for (int index = 0; index < mUidToFadedAppsMap.size(); index++) {
292                 mUidToFadedAppsMap.valueAt(index).dump(pw);
293             }
294         }
295     }
296 
297     //=========================================================================
298     /**
299      * Class to group players from a common app, that are faded out.
300      */
301     private static final class FadedOutApp {
302         private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
303                 new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
304                         .createIfNeeded()
305                         .build();
306 
307         // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp
308         private static final VolumeShaper.Operation PLAY_SKIP_RAMP =
309                 new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build();
310 
311         private final int mUid;
312         // key -> piid; value -> volume shaper config applied
313         private final SparseArray<VolumeShaper.Configuration> mFadedPlayers = new SparseArray<>();
314 
FadedOutApp(int uid)315         FadedOutApp(int uid) {
316             mUid = uid;
317         }
318 
dump(PrintWriter pw)319         void dump(PrintWriter pw) {
320             pw.print("\t uid:" + mUid + " piids:");
321             for (int index = 0; index < mFadedPlayers.size(); index++) {
322                 pw.print("piid: " + mFadedPlayers.keyAt(index) + " Volume shaper: "
323                         + mFadedPlayers.valueAt(index));
324             }
325             pw.println("");
326         }
327 
328         /**
329          * Add this player to the list of faded out players and apply the fade
330          * @param apc a config that satisfies
331          *      apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
332          * @param skipRamp {@code true} if the player should be directly into the end of ramp state.
333          *      This value would for instance be {@code false} when adding players at the start
334          *      of a fade.
335          */
addFade(@onNull AudioPlaybackConfiguration apc, boolean skipRamp, @NonNull VolumeShaper.Configuration volShaper)336         void addFade(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp,
337                 @NonNull VolumeShaper.Configuration volShaper) {
338             final int piid = Integer.valueOf(apc.getPlayerInterfaceId());
339 
340             // positive index return implies player is already faded
341             if (mFadedPlayers.indexOfKey(piid) >= 0) {
342                 if (DEBUG) {
343                     Slog.v(TAG, "player piid:" + piid + " already faded out");
344                 }
345                 return;
346             }
347             if (apc.getPlayerProxy() != null) {
348                 applyVolumeShaperInternal(apc, piid, volShaper,
349                         skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED, skipRamp,
350                         PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT);
351                 mFadedPlayers.put(piid, volShaper);
352             } else {
353                 if (DEBUG) {
354                     Slog.v(TAG, "Error fading out player piid:" + piid
355                             + ", player not found for uid " + mUid);
356                 }
357             }
358         }
359 
removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players)360         void removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players) {
361             for (int index = 0; index < mFadedPlayers.size(); index++) {
362                 int piid = mFadedPlayers.keyAt(index);
363                 final AudioPlaybackConfiguration apc = players.get(piid);
364                 if ((apc != null) && (apc.getPlayerProxy() != null)) {
365                     applyVolumeShaperInternal(apc, piid, /* volShaperConfig= */ null,
366                             VolumeShaper.Operation.REVERSE, /* skipRamp= */ false,
367                             PlaybackActivityMonitor.EVENT_TYPE_FADE_IN);
368                 } else {
369                     // this piid was in the list of faded players, but wasn't found
370                     if (DEBUG) {
371                         Slog.v(TAG, "Error unfading out player piid:" + piid
372                                 + ", player not found for uid " + mUid);
373                     }
374                 }
375             }
376             mFadedPlayers.clear();
377         }
378 
379         @GuardedBy("mLock")
fadeInPlayer(@onNull AudioPlaybackConfiguration apc, @Nullable VolumeShaper.Configuration config)380         void fadeInPlayer(@NonNull AudioPlaybackConfiguration apc,
381                 @Nullable VolumeShaper.Configuration config) {
382             int piid = Integer.valueOf(apc.getPlayerInterfaceId());
383             // if not found, no need to fade in since it was never faded out
384             if (!mFadedPlayers.contains(piid)) {
385                 if (DEBUG) {
386                     Slog.v(TAG, "Player (piid: " + piid + ") for uid (" + mUid
387                             + ") is not faded out, no need to fade in");
388                 }
389                 return;
390             }
391 
392             VolumeShaper.Operation operation = VolumeShaper.Operation.REVERSE;
393             if (config != null) {
394                 // replace and join the volumeshapers with (possibly) in progress fade out operation
395                 // for a smoother fade in
396                 operation = new VolumeShaper.Operation.Builder()
397                         .replace(mFadedPlayers.get(piid).getId(), /* join= */ true).build();
398             }
399             mFadedPlayers.remove(piid);
400             if (apc.getPlayerProxy() != null) {
401                 applyVolumeShaperInternal(apc, piid, config, operation, /* skipRamp= */ false,
402                         PlaybackActivityMonitor.EVENT_TYPE_FADE_IN);
403             } else {
404                 if (DEBUG) {
405                     Slog.v(TAG, "Error fading in player piid:" + piid
406                             + ", player not found for uid " + mUid);
407                 }
408             }
409         }
410 
411         @GuardedBy("mLock")
clear()412         void clear() {
413             if (mFadedPlayers.size() > 0) {
414                 if (DEBUG) {
415                     Slog.v(TAG, "Non empty faded players list being cleared! Faded out players:"
416                             + mFadedPlayers);
417                 }
418             }
419             // should the players be faded in irrespective?
420             mFadedPlayers.clear();
421         }
422 
removeReleased(@onNull AudioPlaybackConfiguration apc)423         void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
424             mFadedPlayers.delete(Integer.valueOf(apc.getPlayerInterfaceId()));
425         }
426 
applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid, VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation, boolean skipRamp, String eventType)427         private void applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid,
428                 VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation,
429                 boolean skipRamp, String eventType) {
430             VolumeShaper.Configuration config = volShaperConfig;
431             // when operation is reverse, use the fade out volume shaper config instead
432             if (operation.equals(VolumeShaper.Operation.REVERSE)) {
433                 config = mFadedPlayers.get(piid);
434             }
435             try {
436                 logFadeEvent(apc, piid, volShaperConfig, operation, skipRamp, eventType);
437                 apc.getPlayerProxy().applyVolumeShaper(config, operation);
438             } catch (Exception e) {
439                 Slog.e(TAG, "Error " + eventType + " piid:" + piid + " uid:" + mUid, e);
440             }
441         }
442 
logFadeEvent(AudioPlaybackConfiguration apc, int piid, VolumeShaper.Configuration config, VolumeShaper.Operation operation, boolean skipRamp, String eventType)443         private void logFadeEvent(AudioPlaybackConfiguration apc, int piid,
444                 VolumeShaper.Configuration config, VolumeShaper.Operation operation,
445                 boolean skipRamp, String eventType) {
446             if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT)) {
447                 PlaybackActivityMonitor.sEventLogger.enqueue(
448                         (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp, config, operation))
449                                 .printLog(TAG));
450                 return;
451             }
452 
453             if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_IN)) {
454                 PlaybackActivityMonitor.sEventLogger.enqueue(
455                         (new PlaybackActivityMonitor.FadeInEvent(apc, skipRamp, config, operation))
456                                 .printLog(TAG));
457                 return;
458             }
459 
460             PlaybackActivityMonitor.sEventLogger.enqueue(
461                     (new EventLogger.StringEvent(eventType + " piid:" + piid)).printLog(TAG));
462         }
463     }
464 }
465