summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jean-Michel Trivi <jmtrivi@google.com> 2021-02-04 07:51:43 -0800
committer Jean-Michel Trivi <jmtrivi@google.com> 2021-03-05 17:00:18 -0800
commitb50f70c8c04ab2ff92ed9425eee2e8545b91b9a1 (patch)
treea488ed9d1fe5d31dc8a7910a8f73eec9ecd19405
parentf98091f2e1fc0f0ee2794f6bc5b226647cfafebd (diff)
Audio focus: fade out active playbacks on FOCUS_LOSS
When an application loses audio focus with AUDIOFOCUS_LOSS and its active players (on usage GAME or MEDIA, but not SPEECH) qualify to be faded out with a VolumeShaper: - apply the fade on the active players - remove the focus loser from the stack - delay the dispatch of focus loss by the duration of the fade Management of who is being faded out is in the FadingManager class. When an app requests audio focus and the request is granted, undo the fade. When a player from an app being faded out starts, apply the fade but skip the volume ramp (essentially muting the player). Bug: 181910599 Test: cts-tradefed run cts -m CtsMediaTestCases -t android.media.cts.AudioFocusTest Change-Id: I5f7b466d30057ce03466ac2edc950a02aca767d6
-rw-r--r--services/core/java/com/android/server/audio/FadeOutManager.java245
-rw-r--r--services/core/java/com/android/server/audio/FocusRequester.java52
-rw-r--r--services/core/java/com/android/server/audio/MediaFocusControl.java81
-rw-r--r--services/core/java/com/android/server/audio/PlaybackActivityMonitor.java106
-rw-r--r--services/core/java/com/android/server/audio/PlayerFocusEnforcer.java23
5 files changed, 489 insertions, 18 deletions
diff --git a/services/core/java/com/android/server/audio/FadeOutManager.java b/services/core/java/com/android/server/audio/FadeOutManager.java
new file mode 100644
index 000000000000..9f0a2baea91d
--- /dev/null
+++ b/services/core/java/com/android/server/audio/FadeOutManager.java
@@ -0,0 +1,245 @@
+/*
+ * 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()));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/audio/FocusRequester.java b/services/core/java/com/android/server/audio/FocusRequester.java
index b6d647289ed8..cc60fe1200b1 100644
--- a/services/core/java/com/android/server/audio/FocusRequester.java
+++ b/services/core/java/com/android/server/audio/FocusRequester.java
@@ -70,6 +70,11 @@ public class FocusRequester {
*/
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
*/
private final @NonNull AudioAttributes mAttributes;
@@ -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;
}
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index b1633b01df90..1dcfdaec44ec 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -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;
+ }
+ }
+ };
+
+ }
}
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 8af1b5be1517..47c91e6c23dd 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -51,8 +51,9 @@ public final class PlaybackActivityMonitor
public static final String TAG = "AudioService.PlaybackActivityMonitor";
- private static final boolean DEBUG = false;
- private static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1;
+ /*package*/ static final boolean DEBUG = false;
+ /*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1;
+ /*package*/ static final int VOLUME_SHAPER_SYSTEM_FADEOUT_ID = 2;
private static final VolumeShaper.Configuration DUCK_VSHAPE =
new VolumeShaper.Configuration.Builder()
@@ -298,6 +299,7 @@ public final class PlaybackActivityMonitor
}
if (change && event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
mDuckingManager.checkDuck(apc);
+ mFadingManager.checkFade(apc);
}
}
if (change) {
@@ -320,6 +322,7 @@ public final class PlaybackActivityMonitor
"releasing player piid:" + piid));
mPlayers.remove(new Integer(piid));
mDuckingManager.removeReleased(apc);
+ mFadingManager.removeReleased(apc);
checkVolumeForPrivilegedAlarm(apc, AudioPlaybackConfiguration.PLAYER_STATE_RELEASED);
change = apc.handleStateEvent(AudioPlaybackConfiguration.PLAYER_STATE_RELEASED,
AudioPlaybackConfiguration.PLAYER_DEVICEID_INVALID);
@@ -442,6 +445,9 @@ public final class PlaybackActivityMonitor
// ducked players
pw.println("\n ducked players piids:");
mDuckingManager.dump(pw);
+ // faded out players
+ pw.println("\n faded out players piids:");
+ mFadingManager.dump(pw);
// players muted due to the device ringing or being in a call
pw.print("\n muted player piids:");
for (int piid : mMutedPlayers) {
@@ -606,10 +612,11 @@ public final class PlaybackActivityMonitor
}
@Override
- public void unduckPlayers(@NonNull FocusRequester winner) {
+ public void restoreVShapedPlayers(@NonNull FocusRequester winner) {
if (DEBUG) { Log.v(TAG, "unduckPlayers: uids winner=" + winner.getClientUid()); }
synchronized (mPlayerLock) {
mDuckingManager.unduckUid(winner.getClientUid(), mPlayers);
+ mFadingManager.unfadeOutUid(winner.getClientUid(), mPlayers);
}
}
@@ -678,6 +685,67 @@ public final class PlaybackActivityMonitor
}
}
+ private final FadeOutManager mFadingManager = new FadeOutManager();
+
+ /**
+ *
+ * @param winner the new non-transient focus owner
+ * @param loser the previous focus owner
+ * @return true if there are players being faded out
+ */
+ @Override
+ public boolean fadeOutPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser) {
+ if (DEBUG) {
+ Log.v(TAG, "fadeOutPlayers: winner=" + winner.getPackageName()
+ + " loser=" + loser.getPackageName());
+ }
+ boolean loserHasActivePlayers = false;
+
+ // find which players to fade out
+ synchronized (mPlayerLock) {
+ if (mPlayers.isEmpty()) {
+ return false;
+ }
+ // check if this UID needs to be faded out (return false if not), and gather list of
+ // eligible players to fade out
+ final Iterator<AudioPlaybackConfiguration> apcIterator = mPlayers.values().iterator();
+ final ArrayList<AudioPlaybackConfiguration> apcsToFadeOut =
+ new ArrayList<AudioPlaybackConfiguration>();
+ while (apcIterator.hasNext()) {
+ final AudioPlaybackConfiguration apc = apcIterator.next();
+ if (!winner.hasSameUid(apc.getClientUid())
+ && loser.hasSameUid(apc.getClientUid())
+ && apc.getPlayerState()
+ == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+ if (!FadeOutManager.canBeFadedOut(apc)) {
+ // the player is not eligible to be faded out, bail
+ Log.v(TAG, "not fading out player " + apc.getPlayerInterfaceId()
+ + " uid:" + apc.getClientUid() + " pid:" + apc.getClientPid()
+ + " type:"
+ + AudioPlaybackConfiguration.toLogFriendlyPlayerType(
+ apc.getPlayerType())
+ + " attr:" + apc.getAudioAttributes());
+ return false;
+ }
+ loserHasActivePlayers = true;
+ apcsToFadeOut.add(apc);
+ }
+ }
+ //###
+ //mDuckingManager.duckUid(loser.getClientUid(), apcsToFadeOut);
+ if (loserHasActivePlayers) {
+ mFadingManager.fadeOutUid(loser.getClientUid(), apcsToFadeOut);
+ }
+ }
+
+ return loserHasActivePlayers;
+ }
+
+ @Override
+ public void forgetUid(int uid) {
+ mFadingManager.forgetUid(uid);
+ }
+
//=================================================================
// Track playback activity listeners
@@ -964,13 +1032,15 @@ public final class PlaybackActivityMonitor
}
}
- private static final class DuckEvent extends AudioEventLogger.Event {
+ private abstract static class VolumeShaperEvent extends AudioEventLogger.Event {
private final int mPlayerIId;
private final boolean mSkipRamp;
private final int mClientUid;
private final int mClientPid;
- DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
+ abstract String getVSAction();
+
+ VolumeShaperEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
mPlayerIId = apc.getPlayerInterfaceId();
mSkipRamp = skipRamp;
mClientUid = apc.getClientUid();
@@ -979,12 +1049,34 @@ public final class PlaybackActivityMonitor
@Override
public String eventToString() {
- return new StringBuilder("ducking player piid:").append(mPlayerIId)
+ return new StringBuilder(getVSAction()).append(" player piid:").append(mPlayerIId)
.append(" uid/pid:").append(mClientUid).append("/").append(mClientPid)
.append(" skip ramp:").append(mSkipRamp).toString();
}
}
+ static final class DuckEvent extends VolumeShaperEvent {
+ @Override
+ String getVSAction() {
+ return "ducking";
+ }
+
+ DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
+ super(apc, skipRamp);
+ }
+ }
+
+ static final class FadeOutEvent extends VolumeShaperEvent {
+ @Override
+ String getVSAction() {
+ return "fading out";
+ }
+
+ FadeOutEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
+ super(apc, skipRamp);
+ }
+ }
+
private static final class AudioAttrEvent extends AudioEventLogger.Event {
private final int mPlayerIId;
private final AudioAttributes mPlayerAttr;
@@ -1000,6 +1092,6 @@ public final class PlaybackActivityMonitor
}
}
- private static final AudioEventLogger sEventLogger = new AudioEventLogger(100,
+ static final AudioEventLogger sEventLogger = new AudioEventLogger(100,
"playback activity as reported through PlayerBase");
}
diff --git a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
index 89e7b7828b15..fb72ac282e8d 100644
--- a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
+++ b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
@@ -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