• 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 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                 if (volShaper != null) {
203                     fa.addFade(apc, /* skipRamp= */ false, volShaper);
204                 }
205             }
206         }
207     }
208 
209     /**
210      * Remove the app for the given UID from the list of faded out apps, unfade out its players
211      * @param uid the uid for the app to unfade out
212      * @param players map of current available players (so we can get an APC from piid)
213      */
unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players)214     void unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players) {
215         Slog.i(TAG, "unfadeOutUid() uid:" + uid);
216         synchronized (mLock) {
217             FadedOutApp fa = mUidToFadedAppsMap.get(uid);
218             if (fa == null) {
219                 return;
220             }
221             mUidToFadedAppsMap.remove(uid);
222 
223             if (!enableFadeManagerConfiguration()) {
224                 fa.removeUnfadeAll(players);
225                 return;
226             }
227 
228             // since fade manager configs may have volume-shaper config per audio attributes,
229             // iterate through each palyer and gather respective configs  for fade in
230             ArrayList<AudioPlaybackConfiguration> apcs = new ArrayList<>(players.values());
231             for (int index = 0; index < apcs.size(); index++) {
232                 AudioPlaybackConfiguration apc = apcs.get(index);
233                 VolumeShaper.Configuration config = mFadeConfigurations
234                         .getFadeInVolumeShaperConfig(apc.getAudioAttributes());
235                 fa.fadeInPlayer(apc, config);
236             }
237             // ideal case all players should be faded in
238             fa.clear();
239         }
240     }
241 
242     // pre-condition: apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
243     //   see {@link PlaybackActivityMonitor#playerEvent}
checkFade(@onNull AudioPlaybackConfiguration apc)244     void checkFade(@NonNull AudioPlaybackConfiguration apc) {
245         if (DEBUG) {
246             Slog.v(TAG, "checkFade() player piid:"
247                     + apc.getPlayerInterfaceId() + " uid:" + apc.getClientUid());
248         }
249 
250         synchronized (mLock) {
251             final VolumeShaper.Configuration volShaper =
252                     mFadeConfigurations.getFadeOutVolumeShaperConfig(apc.getAudioAttributes());
253             final FadedOutApp fa = mUidToFadedAppsMap.get(apc.getClientUid());
254             if (fa == null || volShaper == null) {
255                 return;
256             }
257             fa.addFade(apc, /* skipRamp= */ true, volShaper);
258         }
259     }
260 
261     /**
262      * Remove the player from the list of faded out players because it has been released
263      * @param apc the released player
264      */
removeReleased(@onNull AudioPlaybackConfiguration apc)265     void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
266         final int uid = apc.getClientUid();
267         if (DEBUG) {
268             Slog.v(TAG, "removedReleased() player piid: "
269                     + apc.getPlayerInterfaceId() + " uid:" + uid);
270         }
271         synchronized (mLock) {
272             final FadedOutApp fa = mUidToFadedAppsMap.get(uid);
273             if (fa == null) {
274                 return;
275             }
276             fa.removeReleased(apc);
277         }
278     }
279 
280     /**
281      * Check if uid is currently faded out
282      * @param uid Client id
283      * @return {@code true} if uid is currently faded out. Othwerwise, {@code false}.
284      */
isUidFadedOut(int uid)285     boolean isUidFadedOut(int uid) {
286         synchronized (mLock) {
287             return mUidToFadedAppsMap.contains(uid);
288         }
289     }
290 
dump(PrintWriter pw)291     void dump(PrintWriter pw) {
292         synchronized (mLock) {
293             for (int index = 0; index < mUidToFadedAppsMap.size(); index++) {
294                 mUidToFadedAppsMap.valueAt(index).dump(pw);
295             }
296         }
297     }
298 
299     //=========================================================================
300     /**
301      * Class to group players from a common app, that are faded out.
302      */
303     private static final class FadedOutApp {
304         private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
305                 new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
306                         .createIfNeeded()
307                         .build();
308 
309         // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp
310         private static final VolumeShaper.Operation PLAY_SKIP_RAMP =
311                 new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build();
312 
313         private final int mUid;
314         // key -> piid; value -> volume shaper config applied
315         private final SparseArray<VolumeShaper.Configuration> mFadedPlayers = new SparseArray<>();
316 
FadedOutApp(int uid)317         FadedOutApp(int uid) {
318             mUid = uid;
319         }
320 
dump(PrintWriter pw)321         void dump(PrintWriter pw) {
322             pw.print("\t uid:" + mUid + " piids:");
323             for (int index = 0; index < mFadedPlayers.size(); index++) {
324                 pw.print("piid: " + mFadedPlayers.keyAt(index) + " Volume shaper: "
325                         + mFadedPlayers.valueAt(index));
326             }
327             pw.println("");
328         }
329 
330         /**
331          * Add this player to the list of faded out players and apply the fade
332          * @param apc a config that satisfies
333          *      apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
334          * @param skipRamp {@code true} if the player should be directly into the end of ramp state.
335          *      This value would for instance be {@code false} when adding players at the start
336          *      of a fade.
337          */
addFade(@onNull AudioPlaybackConfiguration apc, boolean skipRamp, @NonNull VolumeShaper.Configuration volShaper)338         void addFade(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp,
339                 @NonNull VolumeShaper.Configuration volShaper) {
340             final int piid = Integer.valueOf(apc.getPlayerInterfaceId());
341 
342             // positive index return implies player is already faded
343             if (mFadedPlayers.indexOfKey(piid) >= 0) {
344                 if (DEBUG) {
345                     Slog.v(TAG, "player piid:" + piid + " already faded out");
346                 }
347                 return;
348             }
349             if (apc.getPlayerProxy() != null) {
350                 applyVolumeShaperInternal(apc, piid, volShaper,
351                         skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED, skipRamp,
352                         PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT);
353                 mFadedPlayers.put(piid, volShaper);
354             } else {
355                 if (DEBUG) {
356                     Slog.v(TAG, "Error fading out player piid:" + piid
357                             + ", player not found for uid " + mUid);
358                 }
359             }
360         }
361 
removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players)362         void removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players) {
363             for (int index = 0; index < mFadedPlayers.size(); index++) {
364                 int piid = mFadedPlayers.keyAt(index);
365                 final AudioPlaybackConfiguration apc = players.get(piid);
366                 if ((apc != null) && (apc.getPlayerProxy() != null)) {
367                     applyVolumeShaperInternal(apc, piid, /* volShaperConfig= */ null,
368                             VolumeShaper.Operation.REVERSE, /* skipRamp= */ false,
369                             PlaybackActivityMonitor.EVENT_TYPE_FADE_IN);
370                 } else {
371                     // this piid was in the list of faded players, but wasn't found
372                     if (DEBUG) {
373                         Slog.v(TAG, "Error unfading out player piid:" + piid
374                                 + ", player not found for uid " + mUid);
375                     }
376                 }
377             }
378             mFadedPlayers.clear();
379         }
380 
381         @GuardedBy("mLock")
fadeInPlayer(@onNull AudioPlaybackConfiguration apc, @Nullable VolumeShaper.Configuration config)382         void fadeInPlayer(@NonNull AudioPlaybackConfiguration apc,
383                 @Nullable VolumeShaper.Configuration config) {
384             int piid = Integer.valueOf(apc.getPlayerInterfaceId());
385             // if not found, no need to fade in since it was never faded out
386             if (!mFadedPlayers.contains(piid)) {
387                 if (DEBUG) {
388                     Slog.v(TAG, "Player (piid: " + piid + ") for uid (" + mUid
389                             + ") is not faded out, no need to fade in");
390                 }
391                 return;
392             }
393 
394             VolumeShaper.Operation operation = VolumeShaper.Operation.REVERSE;
395             if (config != null) {
396                 // replace and join the volumeshapers with (possibly) in progress fade out operation
397                 // for a smoother fade in
398                 operation = new VolumeShaper.Operation.Builder()
399                         .replace(mFadedPlayers.get(piid).getId(), /* join= */ true).build();
400             }
401             mFadedPlayers.remove(piid);
402             if (apc.getPlayerProxy() != null) {
403                 applyVolumeShaperInternal(apc, piid, config, operation, /* skipRamp= */ false,
404                         PlaybackActivityMonitor.EVENT_TYPE_FADE_IN);
405             } else {
406                 if (DEBUG) {
407                     Slog.v(TAG, "Error fading in player piid:" + piid
408                             + ", player not found for uid " + mUid);
409                 }
410             }
411         }
412 
413         @GuardedBy("mLock")
clear()414         void clear() {
415             if (mFadedPlayers.size() > 0) {
416                 if (DEBUG) {
417                     Slog.v(TAG, "Non empty faded players list being cleared! Faded out players:"
418                             + mFadedPlayers);
419                 }
420             }
421             // should the players be faded in irrespective?
422             mFadedPlayers.clear();
423         }
424 
removeReleased(@onNull AudioPlaybackConfiguration apc)425         void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
426             mFadedPlayers.delete(Integer.valueOf(apc.getPlayerInterfaceId()));
427         }
428 
applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid, VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation, boolean skipRamp, String eventType)429         private void applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid,
430                 VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation,
431                 boolean skipRamp, String eventType) {
432             VolumeShaper.Configuration config = volShaperConfig;
433             // when operation is reverse, use the fade out volume shaper config instead
434             if (operation.equals(VolumeShaper.Operation.REVERSE)) {
435                 config = mFadedPlayers.get(piid);
436             }
437             try {
438                 logFadeEvent(apc, piid, volShaperConfig, operation, skipRamp, eventType);
439                 apc.getPlayerProxy().applyVolumeShaper(config, operation);
440             } catch (Exception e) {
441                 Slog.e(TAG, "Error " + eventType + " piid:" + piid + " uid:" + mUid, e);
442             }
443         }
444 
logFadeEvent(AudioPlaybackConfiguration apc, int piid, VolumeShaper.Configuration config, VolumeShaper.Operation operation, boolean skipRamp, String eventType)445         private void logFadeEvent(AudioPlaybackConfiguration apc, int piid,
446                 VolumeShaper.Configuration config, VolumeShaper.Operation operation,
447                 boolean skipRamp, String eventType) {
448             if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_OUT)) {
449                 PlaybackActivityMonitor.sEventLogger.enqueue(
450                         (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp, config, operation))
451                                 .printLog(TAG));
452                 return;
453             }
454 
455             if (eventType.equals(PlaybackActivityMonitor.EVENT_TYPE_FADE_IN)) {
456                 PlaybackActivityMonitor.sEventLogger.enqueue(
457                         (new PlaybackActivityMonitor.FadeInEvent(apc, skipRamp, config, operation))
458                                 .printLog(TAG));
459                 return;
460             }
461 
462             PlaybackActivityMonitor.sEventLogger.enqueue(
463                     (new EventLogger.StringEvent(eventType + " piid:" + piid)).printLog(TAG));
464         }
465     }
466 }
467