| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.audio; |
| |
| import android.annotation.NonNull; |
| import android.media.AudioAttributes; |
| import android.media.AudioManager; |
| import android.media.AudioPlaybackConfiguration; |
| import android.media.VolumeShaper; |
| import android.util.Log; |
| |
| import com.android.internal.util.ArrayUtils; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| |
| /** |
| * Class to handle fading out players |
| */ |
| public final class FadeOutManager { |
| |
| public static final String TAG = "AudioService.FadeOutManager"; |
| |
| /*package*/ static final long FADE_OUT_DURATION_MS = 2000; |
| |
| private static final boolean DEBUG = PlaybackActivityMonitor.DEBUG; |
| |
| private static final VolumeShaper.Configuration FADEOUT_VSHAPE = |
| new VolumeShaper.Configuration.Builder() |
| .setId(PlaybackActivityMonitor.VOLUME_SHAPER_SYSTEM_FADEOUT_ID) |
| .setCurve(new float[]{0.f, 0.25f, 1.0f} /* times */, |
| new float[]{1.f, 0.65f, 0.0f} /* volumes */) |
| .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) |
| .setDuration(FADE_OUT_DURATION_MS) |
| .build(); |
| private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED = |
| new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY) |
| .createIfNeeded() |
| .build(); |
| |
| private static final int[] UNFADEABLE_PLAYER_TYPES = { |
| AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO, |
| AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL, |
| }; |
| |
| private static final int[] UNFADEABLE_CONTENT_TYPES = { |
| AudioAttributes.CONTENT_TYPE_SPEECH, |
| }; |
| |
| private static final int[] FADEABLE_USAGES = { |
| AudioAttributes.USAGE_GAME, |
| AudioAttributes.USAGE_MEDIA, |
| }; |
| |
| // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp |
| private static final VolumeShaper.Operation PLAY_SKIP_RAMP = |
| new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build(); |
| |
| |
| // TODO explore whether a shorter fade out would be a better UX instead of not fading out at all |
| // (legacy behavior) |
| /** |
| * Determine whether the focus request would trigger a fade out, given the parameters of the |
| * requester and those of the focus loser |
| * @param requester the parameters for the focus request |
| * @return true if there can be a fade out over the requester starting to play |
| */ |
| static boolean canCauseFadeOut(@NonNull FocusRequester requester, |
| @NonNull FocusRequester loser) { |
| if (requester.getAudioAttributes().getContentType() == AudioAttributes.CONTENT_TYPE_SPEECH) |
| { |
| if (DEBUG) { Log.i(TAG, "not fading out: new focus is for speech"); } |
| return false; |
| } |
| if ((loser.getGrantFlags() & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0) { |
| if (DEBUG) { Log.i(TAG, "not fading out: loser has PAUSES_ON_DUCKABLE_LOSS"); } |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Evaluates whether the player associated with this configuration can and should be faded out |
| * @param apc the configuration of the player |
| * @return true if player type and AudioAttributes are compatible with fade out |
| */ |
| static boolean canBeFadedOut(@NonNull AudioPlaybackConfiguration apc) { |
| if (ArrayUtils.contains(UNFADEABLE_PLAYER_TYPES, apc.getPlayerType())) { |
| if (DEBUG) { Log.i(TAG, "not fading: player type:" + apc.getPlayerType()); } |
| return false; |
| } |
| if (ArrayUtils.contains(UNFADEABLE_CONTENT_TYPES, |
| apc.getAudioAttributes().getContentType())) { |
| if (DEBUG) { |
| Log.i(TAG, "not fading: content type:" |
| + apc.getAudioAttributes().getContentType()); |
| } |
| return false; |
| } |
| if (!ArrayUtils.contains(FADEABLE_USAGES, apc.getAudioAttributes().getUsage())) { |
| if (DEBUG) { |
| Log.i(TAG, "not fading: usage:" + apc.getAudioAttributes().getUsage()); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| static long getFadeOutDurationOnFocusLossMillis(AudioAttributes aa) { |
| if (ArrayUtils.contains(UNFADEABLE_CONTENT_TYPES, aa.getContentType())) { |
| return 0; |
| } |
| if (!ArrayUtils.contains(FADEABLE_USAGES, aa.getUsage())) { |
| return 0; |
| } |
| return FADE_OUT_DURATION_MS; |
| } |
| |
| /** |
| * Map of uid (key) to faded out apps (value) |
| */ |
| private final HashMap<Integer, FadedOutApp> mFadedApps = new HashMap<Integer, FadedOutApp>(); |
| |
| synchronized void fadeOutUid(int uid, ArrayList<AudioPlaybackConfiguration> players) { |
| Log.i(TAG, "fadeOutUid() uid:" + uid); |
| if (!mFadedApps.containsKey(uid)) { |
| mFadedApps.put(uid, new FadedOutApp(uid)); |
| } |
| final FadedOutApp fa = mFadedApps.get(uid); |
| for (AudioPlaybackConfiguration apc : players) { |
| fa.addFade(apc, false /*skipRamp*/); |
| } |
| } |
| |
| synchronized void unfadeOutUid(int uid, HashMap<Integer, AudioPlaybackConfiguration> players) { |
| Log.i(TAG, "unfadeOutUid() uid:" + uid); |
| final FadedOutApp fa = mFadedApps.remove(uid); |
| if (fa == null) { |
| return; |
| } |
| fa.removeUnfadeAll(players); |
| } |
| |
| synchronized void forgetUid(int uid) { |
| //Log.v(TAG, "forget() uid:" + uid); |
| //mFadedApps.remove(uid); |
| // TODO unfade all players later in case they are reused or the app continued to play |
| } |
| |
| // pre-condition: apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED |
| // see {@link PlaybackActivityMonitor#playerEvent} |
| synchronized void checkFade(@NonNull AudioPlaybackConfiguration apc) { |
| if (DEBUG) { |
| Log.v(TAG, "checkFade() player piid:" |
| + apc.getPlayerInterfaceId() + " uid:" + apc.getClientUid()); |
| } |
| final FadedOutApp fa = mFadedApps.get(apc.getClientUid()); |
| if (fa == null) { |
| return; |
| } |
| fa.addFade(apc, true); |
| } |
| |
| /** |
| * Remove the player from the list of faded out players because it has been released |
| * @param apc the released player |
| */ |
| synchronized void removeReleased(@NonNull AudioPlaybackConfiguration apc) { |
| final int uid = apc.getClientUid(); |
| if (DEBUG) { |
| Log.v(TAG, "removedReleased() player piid: " |
| + apc.getPlayerInterfaceId() + " uid:" + uid); |
| } |
| final FadedOutApp fa = mFadedApps.get(uid); |
| if (fa == null) { |
| return; |
| } |
| fa.removeReleased(apc); |
| } |
| |
| synchronized void dump(PrintWriter pw) { |
| for (FadedOutApp da : mFadedApps.values()) { |
| da.dump(pw); |
| } |
| } |
| |
| //========================================================================= |
| /** |
| * Class to group players from a common app, that are faded out. |
| */ |
| private static final class FadedOutApp { |
| private final int mUid; |
| private final ArrayList<Integer> mFadedPlayers = new ArrayList<Integer>(); |
| |
| FadedOutApp(int uid) { |
| mUid = uid; |
| } |
| |
| void dump(PrintWriter pw) { |
| pw.print("\t uid:" + mUid + " piids:"); |
| for (int piid : mFadedPlayers) { |
| pw.print(" " + piid); |
| } |
| pw.println(""); |
| } |
| |
| /** |
| * Add this player to the list of faded out players and apply the fade |
| * @param apc a config that satisfies |
| * apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED |
| * @param skipRamp true if the player should be directly into the end of ramp state. |
| * This value would for instance be false when adding players at the start of a fade. |
| */ |
| void addFade(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) { |
| final int piid = new Integer(apc.getPlayerInterfaceId()); |
| if (mFadedPlayers.contains(piid)) { |
| if (DEBUG) { |
| Log.v(TAG, "player piid:" + piid + " already faded out"); |
| } |
| return; |
| } |
| try { |
| PlaybackActivityMonitor.sEventLogger.log( |
| (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp)).printLog(TAG)); |
| apc.getPlayerProxy().applyVolumeShaper( |
| FADEOUT_VSHAPE, |
| skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED); |
| mFadedPlayers.add(piid); |
| } catch (Exception e) { |
| Log.e(TAG, "Error fading out player piid:" + piid |
| + " uid:" + apc.getClientUid(), e); |
| } |
| } |
| |
| void removeUnfadeAll(HashMap<Integer, AudioPlaybackConfiguration> players) { |
| for (int piid : mFadedPlayers) { |
| final AudioPlaybackConfiguration apc = players.get(piid); |
| if (apc != null) { |
| try { |
| PlaybackActivityMonitor.sEventLogger.log( |
| (new AudioEventLogger.StringEvent("unfading out piid:" |
| + piid)).printLog(TAG)); |
| apc.getPlayerProxy().applyVolumeShaper( |
| FADEOUT_VSHAPE, |
| VolumeShaper.Operation.REVERSE); |
| } catch (Exception e) { |
| Log.e(TAG, "Error unfading out player piid:" + piid + " uid:" + mUid, e); |
| } |
| } else { |
| // this piid was in the list of faded players, but wasn't found |
| if (DEBUG) { |
| Log.v(TAG, "Error unfading out player piid:" + piid |
| + ", player not found for uid " + mUid); |
| } |
| } |
| } |
| mFadedPlayers.clear(); |
| } |
| |
| void removeReleased(@NonNull AudioPlaybackConfiguration apc) { |
| mFadedPlayers.remove(new Integer(apc.getPlayerInterfaceId())); |
| } |
| } |
| } |