Loading services/core/java/com/android/server/audio/FadeOutManager.java 0 → 100644 +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())); } } } services/core/java/com/android/server/audio/FocusRequester.java +48 −4 Original line number Diff line number Diff line Loading @@ -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 */ Loading Loading @@ -102,6 +107,7 @@ public class FocusRequester { mGrantFlags = grantFlags; mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE; mFocusLossWasNotified = true; mFocusLossFadeLimbo = false; mFocusController = ctlr; mSdkTarget = sdk; } Loading @@ -115,6 +121,7 @@ public class FocusRequester { mFocusGainRequest = afi.getGainRequest(); mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE; mFocusLossWasNotified = true; mFocusLossFadeLimbo = false; mGrantFlags = afi.getFlags(); mSdkTarget = afi.getSdkTarget(); Loading @@ -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); } Loading Loading @@ -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; Loading Loading @@ -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; Loading @@ -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); } Loading @@ -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); } } Loading Loading @@ -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 */); Loading Loading @@ -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; } Loading services/core/java/com/android/server/audio/MediaFocusControl.java +77 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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) { Loading @@ -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 Loading @@ -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 //========================================================================================== Loading Loading @@ -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(); } Loading @@ -318,7 +338,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { abandonSource = fr.toAudioFocusInfo(); } // stack entry not used anymore, clear references fr.release(); fr.maybeRelease(); } } } Loading Loading @@ -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; } } }; } } services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +99 −7 File changed.Preview size limit exceeded, changes collapsed. Show changes services/core/java/com/android/server/audio/PlayerFocusEnforcer.java +20 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading
services/core/java/com/android/server/audio/FadeOutManager.java 0 → 100644 +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())); } } }
services/core/java/com/android/server/audio/FocusRequester.java +48 −4 Original line number Diff line number Diff line Loading @@ -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 */ Loading Loading @@ -102,6 +107,7 @@ public class FocusRequester { mGrantFlags = grantFlags; mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE; mFocusLossWasNotified = true; mFocusLossFadeLimbo = false; mFocusController = ctlr; mSdkTarget = sdk; } Loading @@ -115,6 +121,7 @@ public class FocusRequester { mFocusGainRequest = afi.getGainRequest(); mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE; mFocusLossWasNotified = true; mFocusLossFadeLimbo = false; mGrantFlags = afi.getFlags(); mSdkTarget = afi.getSdkTarget(); Loading @@ -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); } Loading Loading @@ -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; Loading Loading @@ -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; Loading @@ -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); } Loading @@ -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); } } Loading Loading @@ -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 */); Loading Loading @@ -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; } Loading
services/core/java/com/android/server/audio/MediaFocusControl.java +77 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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) { Loading @@ -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 Loading @@ -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 //========================================================================================== Loading Loading @@ -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(); } Loading @@ -318,7 +338,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { abandonSource = fr.toAudioFocusInfo(); } // stack entry not used anymore, clear references fr.release(); fr.maybeRelease(); } } } Loading Loading @@ -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; } } }; } }
services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +99 −7 File changed.Preview size limit exceeded, changes collapsed. Show changes
services/core/java/com/android/server/audio/PlayerFocusEnforcer.java +20 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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