Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4c38e34d authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Audio focus: fade out active playbacks on FOCUS_LOSS" into sc-dev

parents c7915589 b50f70c8
Loading
Loading
Loading
Loading
+245 −0
Original line number Diff line number Diff line
/*
 * 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.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 = 2500;

    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, 1.0f} /* times */,
                            new float[]{1.f, 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();

    /**
     * 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;
    }

    /**
     * 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()));
        }
    }
}
+48 −4
Original line number Diff line number Diff line
@@ -69,6 +69,11 @@ public class FocusRequester {
     * whether this focus owner listener was notified when it lost focus
     */
    private boolean mFocusLossWasNotified;
    /**
     * whether this focus owner has already lost focus, but is being faded out until focus loss
     * dispatch occurs. It's in "limbo" mode: has lost focus but not released yet until notified
     */
    boolean mFocusLossFadeLimbo;
    /**
     * the audio attributes associated with the focus request
     */
@@ -102,6 +107,7 @@ public class FocusRequester {
        mGrantFlags = grantFlags;
        mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE;
        mFocusLossWasNotified = true;
        mFocusLossFadeLimbo = false;
        mFocusController = ctlr;
        mSdkTarget = sdk;
    }
@@ -115,6 +121,7 @@ public class FocusRequester {
        mFocusGainRequest = afi.getGainRequest();
        mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE;
        mFocusLossWasNotified = true;
        mFocusLossFadeLimbo = false;
        mGrantFlags = afi.getFlags();
        mSdkTarget = afi.getSdkTarget();

@@ -132,6 +139,13 @@ public class FocusRequester {
        return ((mGrantFlags & AudioManager.AUDIOFOCUS_FLAG_LOCK) != 0);
    }

    /**
     * @return true if the focus requester is scheduled to receive a focus loss
     */
    boolean isInFocusLossLimbo() {
        return mFocusLossFadeLimbo;
    }

    boolean hasSameBinder(IBinder ib) {
        return (mSourceRef != null) && mSourceRef.equals(ib);
    }
@@ -231,11 +245,21 @@ public class FocusRequester {
                + " -- flags: " + flagsToString(mGrantFlags)
                + " -- loss: " + focusLossToString()
                + " -- notified: " + mFocusLossWasNotified
                + " -- limbo" + mFocusLossFadeLimbo
                + " -- uid: " + mCallingUid
                + " -- attr: " + mAttributes
                + " -- sdk:" + mSdkTarget);
    }

    /**
     * Clear all references, except for instances in "loss limbo" due to the current fade out
     * for which there will be an attempt to be clear after the loss has been notified
     */
    void maybeRelease() {
        if (!mFocusLossFadeLimbo) {
            release();
        }
    }

    void release() {
        final IBinder srcRef = mSourceRef;
@@ -315,6 +339,7 @@ public class FocusRequester {
    void handleFocusGain(int focusGain) {
        try {
            mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE;
            mFocusLossFadeLimbo = false;
            mFocusController.notifyExtPolicyFocusGrant_syncAf(toAudioFocusInfo(),
                    AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
            final IAudioFocusDispatcher fd = mFocusDispatcher;
@@ -327,7 +352,7 @@ public class FocusRequester {
                    fd.dispatchAudioFocusChange(focusGain, mClientId);
                }
            }
            mFocusController.unduckPlayers(this);
            mFocusController.restoreVShapedPlayers(this);
        } catch (android.os.RemoteException e) {
            Log.e(TAG, "Failure to signal gain of audio focus due to: ", e);
        }
@@ -336,7 +361,7 @@ public class FocusRequester {
    @GuardedBy("MediaFocusControl.mAudioFocusLock")
    void handleFocusGainFromRequest(int focusRequestResult) {
        if (focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            mFocusController.unduckPlayers(this);
            mFocusController.restoreVShapedPlayers(this);
        }
    }

@@ -375,7 +400,7 @@ public class FocusRequester {
                if (handled) {
                    if (DEBUG) {
                        Log.v(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived)
                            + " to " + mClientId + ", ducking implemented by framework");
                                + " to " + mClientId + ", response handled by framework");
                    }
                    mFocusController.notifyExtPolicyFocusLoss_syncAf(
                            toAudioFocusInfo(), false /* wasDispatched */);
@@ -435,8 +460,27 @@ public class FocusRequester {
                return false;
            }

            return mFocusController.duckPlayers(frWinner, this, forceDuck);
            return mFocusController.duckPlayers(frWinner, /*loser*/ this, forceDuck);
        }

        if (focusLoss == AudioManager.AUDIOFOCUS_LOSS) {
            if (!MediaFocusControl.ENFORCE_FADEOUT_FOR_FOCUS_LOSS) {
                return false;
            }

            // candidate for fade-out before a receiving a loss
            boolean playersAreFaded =  mFocusController.fadeOutPlayers(frWinner, /* loser */ this);
            if (playersAreFaded) {
                // active players are being faded out, delay the dispatch of focus loss
                // mark this instance as being faded so it's not released yet as the focus loss
                // will be dispatched later, it is now in limbo mode
                mFocusLossFadeLimbo = true;
                mFocusController.postDelayedLossAfterFade(this,
                        FadeOutManager.FADE_OUT_DURATION_MS);
                return true;
            }
        }

        return false;
    }

+77 −4
Original line number Diff line number Diff line
@@ -30,7 +30,10 @@ import android.media.MediaMetrics;
import android.media.audiopolicy.IAudioPolicyCallback;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
@@ -80,6 +83,12 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
     */
    static final boolean ENFORCE_MUTING_FOR_RING_OR_CALL = true;

    /**
     * set to true so the framework enforces fading out apps that lose audio focus in a
     * non-transient way.
     */
    static final boolean ENFORCE_FADEOUT_FOR_FOCUS_LOSS = true;

    private final Context mContext;
    private final AppOpsManager mAppOps;
    private PlayerFocusEnforcer mFocusEnforcer; // never null
@@ -98,6 +107,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
        final ContentResolver cr = mContext.getContentResolver();
        mMultiAudioFocusEnabled = Settings.System.getIntForUser(cr,
                Settings.System.MULTI_AUDIO_FOCUS_ENABLED, 0, cr.getUserId()) != 0;
        initFocusThreading();
    }

    protected void dump(PrintWriter pw) {
@@ -119,8 +129,8 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
    }

    @Override
    public void unduckPlayers(@NonNull FocusRequester winner) {
        mFocusEnforcer.unduckPlayers(winner);
    public void restoreVShapedPlayers(@NonNull FocusRequester winner) {
        mFocusEnforcer.restoreVShapedPlayers(winner);
    }

    @Override
@@ -133,6 +143,16 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
        mFocusEnforcer.unmutePlayersForCall();
    }

    @Override
    public boolean fadeOutPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser) {
        return mFocusEnforcer.fadeOutPlayers(winner, loser);
    }

    @Override
    public void forgetUid(int uid) {
        mFocusEnforcer.forgetUid(uid);
    }

    //==========================================================================================
    // AudioFocus
    //==========================================================================================
@@ -294,7 +314,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
        {
            //Log.i(TAG, "   removeFocusStackEntry() removing top of stack");
            FocusRequester fr = mFocusStack.pop();
            fr.release();
            fr.maybeRelease();
            if (notifyFocusFollowers) {
                abandonSource = fr.toAudioFocusInfo();
            }
@@ -318,7 +338,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
                        abandonSource = fr.toAudioFocusInfo();
                    }
                    // stack entry not used anymore, clear references
                    fr.release();
                    fr.maybeRelease();
                }
            }
        }
@@ -1134,4 +1154,57 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
            pw.println("------------------------------");
        }
    }

    //=================================================================
    // Async focus events
    void postDelayedLossAfterFade(FocusRequester focusLoser, long delayMs) {
        if (DEBUG) {
            Log.v(TAG, "postDelayedLossAfterFade loser=" + focusLoser.getPackageName());
        }
        mFocusHandler.sendMessageDelayed(
                mFocusHandler.obtainMessage(MSG_L_FOCUS_LOSS_AFTER_FADE, focusLoser),
                FadeOutManager.FADE_OUT_DURATION_MS);
    }
    //=================================================================
    // Message handling
    private Handler mFocusHandler;
    private HandlerThread mFocusThread;

    /**
     * dispatch a focus loss after an app has been faded out. Focus loser is to be released
     * after dispatch as it has already left the stack
     * args:
     *     msg.obj: the audio focus loser
     *         type:FocusRequester
     */
    private static final int MSG_L_FOCUS_LOSS_AFTER_FADE = 1;

    private void initFocusThreading() {
        mFocusThread = new HandlerThread(TAG);
        mFocusThread.start();
        mFocusHandler = new Handler(mFocusThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_L_FOCUS_LOSS_AFTER_FADE:
                        if (DEBUG) {
                            Log.d(TAG, "MSG_L_FOCUS_LOSS_AFTER_FADE loser="
                                    + ((FocusRequester) msg.obj).getPackageName());
                        }
                        synchronized (mAudioFocusLock) {
                            final FocusRequester loser = (FocusRequester) msg.obj;
                            if (loser.isInFocusLossLimbo()) {
                                loser.dispatchFocusChange(AudioManager.AUDIOFOCUS_LOSS);
                                loser.release();
                                mFocusEnforcer.forgetUid(loser.getClientUid());
                            }
                        }
                        break;
                    default:
                        break;
                }
            }
        };

    }
}
+99 −7

File changed.

Preview size limit exceeded, changes collapsed.

+20 −3
Original line number Diff line number Diff line
@@ -31,11 +31,12 @@ public interface PlayerFocusEnforcer {
                               boolean forceDuck);

    /**
     * Unduck the players that had been ducked with
     * {@link #duckPlayers(FocusRequester, FocusRequester, boolean)}
     * Restore the initial state of any players that had had a volume ramp applied as the result
     * of a duck or fade out through {@link #duckPlayers(FocusRequester, FocusRequester, boolean)}
     * or {@link #fadeOutPlayers(FocusRequester, FocusRequester)}
     * @param winner
     */
    void unduckPlayers(@NonNull FocusRequester winner);
    void restoreVShapedPlayers(@NonNull FocusRequester winner);

    /**
     * Mute players at the beginning of a call
@@ -47,4 +48,20 @@ public interface PlayerFocusEnforcer {
     * Unmute players at the end of a call
     */
    void unmutePlayersForCall();

    /**
     * Fade out whatever is still playing after the non-transient focus change
     * @param winner the new non-transient focus owner
     * @param loser the previous focus owner
     * @return true if there were any active players for the loser that qualified for being
     *         faded out (because of audio attributes, or player types), and as such were faded
     *         out.
     */
    boolean fadeOutPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser);

    /**
     * Mark this UID as no longer playing a role in focus enforcement
     * @param uid
     */
    void forgetUid(int uid);
}
 No newline at end of file