Audio service: only duck started players + refactor

Refactor management of list of ducked players:
  DuckingManager has a list of DuckedApps, which reference
  the ducked players per uid.
Only consider ducking a player when it is in STARTED state.
When a player is released, remove it from the list of ducked players.

Test: play audio in GPM while having driving directions, music ducks
Bug: 37433811
Change-Id: I038a963432c0df6c9470a3a4fb80049d55e8719c
(cherry picked from commit cb84fc010998462b222fa5f784bcc54c9a829b36)
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index d35104f..3053879 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -18,12 +18,10 @@
 
 import android.annotation.NonNull;
 import android.media.AudioAttributes;
-import android.media.AudioFormat;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
 import android.media.AudioSystem;
 import android.media.IPlaybackConfigDispatcher;
-import android.media.MediaRecorder;
 import android.media.PlayerBase;
 import android.media.VolumeShaper;
 import android.os.Binder;
@@ -99,13 +97,6 @@
         apc.init();
         synchronized(mPlayerLock) {
             mPlayers.put(newPiid, apc);
-            if (mDuckedUids.contains(new Integer(apc.getClientUid()))) {
-                if (DEBUG) { Log.v(TAG, "  > trackPlayer() piid=" + newPiid + " must be ducked"); }
-                mDuckedPlayers.add(new Integer(newPiid));
-                // FIXME here the player needs to be put in a state that is the same as if it
-                //   had been ducked as it starts. At the moment, this works already for linked
-                //   players, as is the case in gapless playback.
-            }
         }
         return newPiid;
     }
@@ -131,10 +122,11 @@
         final boolean change;
         synchronized(mPlayerLock) {
             final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
-            // FIXME SoundPool not ready for state reporting
-            if (apc != null
-                    && apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL)
-            {
+            if (apc == null) {
+                return;
+            }
+            if (apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+                // FIXME SoundPool not ready for state reporting
                 return;
             }
             if (checkConfigurationCaller(piid, apc, binderUid)) {
@@ -144,12 +136,8 @@
                 Log.e(TAG, "Error handling event " + event);
                 change = false;
             }
-            if (change && event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
-                    && mDuckedUids.contains(new Integer(apc.getClientUid()))) {
-                if (DEBUG) { Log.v(TAG, "  > playerEvent() piid=" + piid + " must be ducked"); }
-                if (!mDuckedPlayers.contains(new Integer(piid))) {
-                    mDuckedPlayers.add(new Integer(piid));
-                }
+            if (change && event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                mDuckingManager.checkDuck(apc);
             }
         }
         if (change) {
@@ -163,6 +151,7 @@
             final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
             if (checkConfigurationCaller(piid, apc, binderUid)) {
                 mPlayers.remove(new Integer(piid));
+                mDuckingManager.removeReleased(apc);
             }
         }
     }
@@ -182,10 +171,8 @@
                 conf.dump(pw);
             }
             // ducked players
-            pw.println("\n  ducked player piids:");
-            for (int piid : mDuckedPlayers) {
-                pw.println(" " + piid);
-            }
+            pw.println("\n  ducked players:");
+            mDuckingManager.dump(pw);
             // players muted due to the device ringing or being in a call
             pw.println("\n  muted player piids:");
             for (int piid : mMutedPlayers) {
@@ -274,10 +261,9 @@
 
     //=================================================================
     // PlayerFocusEnforcer implementation
-    private final ArrayList<Integer> mDuckedPlayers = new ArrayList<Integer>();
     private final ArrayList<Integer> mMutedPlayers = new ArrayList<Integer>();
-    // size of 2 for typical cases of double-ducking, not expected to grow beyond that, but can
-    private final ArrayList<Integer> mDuckedUids = new ArrayList<Integer>(2);
+
+    private final DuckingManager mDuckingManager = new DuckingManager();
 
     @Override
     public boolean duckPlayers(FocusRequester winner, FocusRequester loser) {
@@ -286,60 +272,46 @@
                     winner.getClientUid(), loser.getClientUid()));
         }
         synchronized (mPlayerLock) {
-            final Integer loserUid = new Integer(loser.getClientUid());
-            if (!mDuckedUids.contains(loserUid)) {
-                mDuckedUids.add(loserUid);
-            }
             if (mPlayers.isEmpty()) {
                 return true;
             }
-            final Set<Integer> piidSet = mPlayers.keySet();
-            final Iterator<Integer> piidIterator = piidSet.iterator();
-            // find which players to duck
-            while (piidIterator.hasNext()) {
-                final Integer piid = piidIterator.next();
-                final AudioPlaybackConfiguration apc = mPlayers.get(piid);
-                if (apc == null) {
-                    continue;
-                }
+            // check if this UID needs to be ducked (return false if not), and gather list of
+            // eligible players to duck
+            final Iterator<AudioPlaybackConfiguration> apcIterator = mPlayers.values().iterator();
+            final ArrayList<AudioPlaybackConfiguration> apcsToDuck =
+                    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 (mDuckedPlayers.contains(new Integer(piid))) {
-                        if (DEBUG) { Log.v(TAG, "player " + piid + " already ducked"); }
-                    } else if (MediaFocusControl.ENFORCE_DUCKING
+                    if (MediaFocusControl.ENFORCE_DUCKING
                             && MediaFocusControl.ENFORCE_DUCKING_FOR_NEW
                             && loser.getSdkTarget() <= MediaFocusControl.DUCKING_IN_APP_SDK_LEVEL) {
                         // legacy behavior, apps used to be notified when they should be ducking
-                        if (DEBUG) { Log.v(TAG, "not ducking player " + piid + ": old SDK"); }
+                        if (DEBUG) {Log.v(TAG, "not ducking player " + apc.getPlayerInterfaceId()
+                                + ": old SDK"); }
                         return false;
                     } else if (apc.getAudioAttributes().getContentType() ==
                             AudioAttributes.CONTENT_TYPE_SPEECH) {
                         // the player is speaking, ducking will make the speech unintelligible
                         // so let the app handle it instead
-                        if (DEBUG) { Log.v(TAG, "not ducking player " + piid + ": SPEECH"); }
+                        if (DEBUG) { Log.v(TAG, "not ducking player " + apc.getPlayerInterfaceId()
+                                + ": SPEECH"); }
                         return false;
                     } else if (apc.getPlayerType()
                             == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
                         // TODO support ducking of SoundPool players
                         return false;
-                    } else {
-                        try {
-                            Log.v(TAG, "ducking player " + piid);
-                            apc.getPlayerProxy().applyVolumeShaper(
-                                    DUCK_VSHAPE,
-                                    PLAY_CREATE_IF_NEEDED);
-                            mDuckedPlayers.add(new Integer(piid));
-                        } catch (Exception e) {
-                            Log.e(TAG, "Error ducking player " + piid, e);
-                            // something went wrong trying to duck, so let the app handle it
-                            // instead, it may know things we don't
-                            return false;
-                        }
                     }
+                    apcsToDuck.add(apc);
                 }
             }
+            // add the players eligible for ducking to the list, and duck them
+            // (if apcsToDuck is empty, this will at least mark this uid as ducked, so when
+            //  players of the same uid start, they will be ducked by DuckingManager.checkDuck())
+            mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck);
         }
         return true;
     }
@@ -348,37 +320,7 @@
     public void unduckPlayers(FocusRequester winner) {
         if (DEBUG) { Log.v(TAG, "unduckPlayers: uids winner=" + winner.getClientUid()); }
         synchronized (mPlayerLock) {
-            if (mDuckedPlayers.isEmpty()) {
-                mDuckedUids.remove(new Integer(winner.getClientUid()));
-                return;
-            }
-            final ArrayList<Integer> playersToRemove =
-                    new ArrayList<Integer>(mDuckedPlayers.size());
-            for (int piid : mDuckedPlayers) {
-                final AudioPlaybackConfiguration apc = mPlayers.get(piid);
-                if (apc != null) {
-                    if (winner.hasSameUid(apc.getClientUid())) {
-                        try {
-                            Log.v(TAG, "unducking player " + piid);
-                            apc.getPlayerProxy().applyVolumeShaper(
-                                    DUCK_ID,
-                                    VolumeShaper.Operation.REVERSE);
-                        } catch (Exception e) {
-                            Log.e(TAG, "Error unducking player " + piid, e);
-                        } finally {
-                            playersToRemove.add(piid);
-                        }
-                    }
-                } else {
-                    // this piid was in the list of ducked players, but wasn't found, discard it
-                    Log.v(TAG, "Error unducking player " + piid + ", player not found");
-                    playersToRemove.add(piid);
-                }
-            }
-            for (int piid : playersToRemove) {
-                mDuckedPlayers.remove(new Integer(piid));
-            }
-            mDuckedUids.remove(new Integer(winner.getClientUid()));
+            mDuckingManager.unduckUid(winner.getClientUid(), mPlayers);
         }
     }
 
@@ -541,4 +483,118 @@
             mDispatcherCb.asBinder().unlinkToDeath(this, 0);
         }
     }
+
+    //=================================================================
+    // Class to handle ducking related operations for a given UID
+    private static final class DuckingManager {
+        private final HashMap<Integer, DuckedApp> mDuckers = new HashMap<Integer, DuckedApp>();
+
+        void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck) {
+            if (!mDuckers.containsKey(uid)) {
+                mDuckers.put(uid, new DuckedApp(uid));
+            }
+            final DuckedApp da = mDuckers.get(uid);
+            for (AudioPlaybackConfiguration apc : apcsToDuck) {
+                da.addDuck(apc);
+            }
+        }
+
+        void unduckUid(int uid, HashMap<Integer, AudioPlaybackConfiguration> players) {
+            final DuckedApp da = mDuckers.remove(uid);
+            if (da == null) {
+                return;
+            }
+            da.removeUnduckAll(players);
+        }
+
+        // pre-condition: apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
+        void checkDuck(@NonNull AudioPlaybackConfiguration apc) {
+            final DuckedApp da = mDuckers.get(apc.getClientUid());
+            if (da == null) {
+                return;
+            }
+            // FIXME here the player needs to be put in a state that is the same as if it
+            //   had been ducked as it starts. At the moment, this works already for linked
+            //   players, as is the case in gapless playback.
+            da.addDuck(apc);
+        }
+
+        void dump(PrintWriter pw) {
+            for (DuckedApp da : mDuckers.values()) {
+                da.dump(pw);
+            }
+        }
+
+        void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
+            final DuckedApp da = mDuckers.get(apc.getClientUid());
+            if (da == null) {
+                return;
+            }
+            da.removeReleased(apc);
+        }
+
+        private static final class DuckedApp {
+            private final int mUid;
+            private final ArrayList<Integer> mDuckedPlayers = new ArrayList<Integer>();
+
+            DuckedApp(int uid) {
+                mUid = uid;
+            }
+
+            void dump(PrintWriter pw) {
+                pw.print("\t uid:" + mUid + " piids:");
+                for (int piid : mDuckedPlayers) {
+                    pw.print(" " + piid);
+                }
+                pw.println("");
+            }
+
+            // pre-conditions:
+            //  * apc != null
+            //  * apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED
+            void addDuck(@NonNull AudioPlaybackConfiguration apc) {
+                final int piid = new Integer(apc.getPlayerInterfaceId());
+                if (mDuckedPlayers.contains(piid)) {
+                    if (DEBUG) { Log.v(TAG, "player " + piid + " already ducked"); }
+                    return;
+                }
+                try {
+                    Log.v(TAG, "ducking player " + apc.getPlayerInterfaceId());
+                    apc.getPlayerProxy().applyVolumeShaper(
+                            DUCK_VSHAPE,
+                            PLAY_CREATE_IF_NEEDED);
+                    mDuckedPlayers.add(piid);
+                } catch (Exception e) {
+                    Log.e(TAG, "Error ducking player " + piid, e);
+                }
+            }
+
+            void removeUnduckAll(HashMap<Integer, AudioPlaybackConfiguration> players) {
+                for (int piid : mDuckedPlayers) {
+                    final AudioPlaybackConfiguration apc = players.get(piid);
+                    if (apc != null) {
+                        try {
+                            Log.v(TAG, "unducking player " + piid);
+                            apc.getPlayerProxy().applyVolumeShaper(
+                                    DUCK_ID,
+                                    VolumeShaper.Operation.REVERSE);
+                        } catch (Exception e) {
+                            Log.e(TAG, "Error unducking player " + piid, e);
+                        }
+                    } else {
+                        // this piid was in the list of ducked players, but wasn't found
+                        if (DEBUG) {
+                            Log.v(TAG, "Error unducking player " + piid + ", player not found for"
+                                    + " uid " + mUid);
+                        }
+                    }
+                }
+                mDuckedPlayers.clear();
+            }
+
+            void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
+                mDuckedPlayers.remove(new Integer(apc.getPlayerInterfaceId()));
+            }
+        }
+    }
 }