Merge "Call listeners whenever players are removed"
diff --git a/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java b/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java
index be223f1..d71c3b0 100644
--- a/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java
+++ b/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java
@@ -16,7 +16,7 @@
 
 package com.android.server.media;
 
-import android.annotation.Nullable;
+import android.annotation.NonNull;
 import android.content.Context;
 import android.media.AudioPlaybackConfiguration;
 import android.media.IAudioService;
@@ -27,14 +27,15 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
 
 import java.io.PrintWriter;
-import java.util.HashSet;
-import java.util.HashMap;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -49,47 +50,57 @@
     private static AudioPlayerStateMonitor sInstance = new AudioPlayerStateMonitor();
 
     /**
-     * Called when the state of audio player is changed.
+     * Listener for handling the active state changes of audio players.
      */
-    interface OnAudioPlayerStateChangedListener {
-        void onAudioPlayerStateChanged(
-                int uid, int prevState, @Nullable AudioPlaybackConfiguration config);
+    interface OnAudioPlayerActiveStateChangedListener {
+        /**
+         * Called when the active state of audio player is changed.
+         *
+         * @param config The audio playback configuration for the audio player of which active state
+         *              was changed. If {@param isRemoved} is {@code true}, this hold outdated
+         *              information.
+         * @param isRemoved {@code true} if the audio player is removed.
+         */
+        void onAudioPlayerActiveStateChanged(
+                @NonNull AudioPlaybackConfiguration config, boolean isRemoved);
     }
 
     private final static class MessageHandler extends Handler {
-        private static final int MSG_AUDIO_PLAYER_STATE_CHANGED = 1;
+        private static final int MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED = 1;
 
-        private final OnAudioPlayerStateChangedListener mListsner;
+        private final OnAudioPlayerActiveStateChangedListener mListener;
 
-        public MessageHandler(Looper looper, OnAudioPlayerStateChangedListener listener) {
+        public MessageHandler(Looper looper, OnAudioPlayerActiveStateChangedListener listener) {
             super(looper);
-            mListsner = listener;
+            mListener = listener;
         }
 
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_AUDIO_PLAYER_STATE_CHANGED:
-                    mListsner.onAudioPlayerStateChanged(
-                            msg.arg1, msg.arg2, (AudioPlaybackConfiguration) msg.obj);
+                case MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED:
+                    mListener.onAudioPlayerActiveStateChanged((AudioPlaybackConfiguration) msg.obj,
+                            msg.arg1 != 0);
                     break;
             }
         }
 
-        public void sendAudioPlayerStateChangedMessage(int uid, int prevState,
-                AudioPlaybackConfiguration config) {
-            obtainMessage(MSG_AUDIO_PLAYER_STATE_CHANGED, uid, prevState, config).sendToTarget();
+        public void sendAudioPlayerActiveStateChangedMessage(
+                final AudioPlaybackConfiguration config, final boolean isRemoved) {
+            obtainMessage(MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED,
+                    isRemoved ? 1 : 0, 0 /* unused */, config).sendToTarget();
         }
     }
 
     private final Object mLock = new Object();
     @GuardedBy("mLock")
-    private final Map<OnAudioPlayerStateChangedListener, MessageHandler> mListenerMap =
-            new HashMap<>();
+    private final Map<OnAudioPlayerActiveStateChangedListener, MessageHandler> mListenerMap =
+            new ArrayMap<>();
     @GuardedBy("mLock")
-    private final Map<Integer, Integer> mAudioPlayerStates = new HashMap<>();
+    private final Set<Integer> mActiveAudioUids = new ArraySet();
     @GuardedBy("mLock")
-    private final Map<Integer, HashSet<Integer>> mAudioPlayersForUid = new HashMap<>();
+    private ArrayMap<Integer, AudioPlaybackConfiguration> mPrevActiveAudioPlaybackConfigs =
+            new ArrayMap<>();
     // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video)
     // The UID whose audio playback becomes active at the last comes first.
     // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID.
@@ -122,32 +133,24 @@
         }
         final long token = Binder.clearCallingIdentity();
         try {
-            final Map<Integer, Integer> prevAudioPlayerStates = new HashMap<>(mAudioPlayerStates);
-            final Map<Integer, HashSet<Integer>> prevAudioPlayersForUid =
-                    new HashMap<>(mAudioPlayersForUid);
             synchronized (mLock) {
-                mAudioPlayerStates.clear();
-                mAudioPlayersForUid.clear();
+                // Update mActiveAudioUids
+                mActiveAudioUids.clear();
+                ArrayMap<Integer, AudioPlaybackConfiguration> activeAudioPlaybackConfigs =
+                        new ArrayMap<>();
                 for (AudioPlaybackConfiguration config : configs) {
-                    int pii = config.getPlayerInterfaceId();
-                    int uid = config.getClientUid();
-                    mAudioPlayerStates.put(pii, config.getPlayerState());
-                    HashSet<Integer> players = mAudioPlayersForUid.get(uid);
-                    if (players == null) {
-                        players = new HashSet<Integer>();
-                        players.add(pii);
-                        mAudioPlayersForUid.put(uid, players);
-                    } else {
-                        players.add(pii);
+                    if (config.isActive()) {
+                        mActiveAudioUids.add(config.getClientUid());
+                        activeAudioPlaybackConfigs.put(config.getPlayerInterfaceId(), config);
                     }
                 }
-                for (AudioPlaybackConfiguration config : configs) {
-                    if (!config.isActive()) {
-                        continue;
-                    }
 
-                    int uid = config.getClientUid();
-                    if (!isActiveState(prevAudioPlayerStates.get(config.getPlayerInterfaceId()))) {
+                // Update mSortedAuioPlaybackClientUids.
+                for (int i = 0; i < activeAudioPlaybackConfigs.size(); ++i) {
+                    AudioPlaybackConfiguration config = activeAudioPlaybackConfigs.valueAt(i);
+                    final int uid = config.getClientUid();
+                    if (!mPrevActiveAudioPlaybackConfigs.containsKey(
+                            config.getPlayerInterfaceId())) {
                         if (DEBUG) {
                             Log.d(TAG, "Found a new active media playback. " +
                                     AudioPlaybackConfiguration.toLogFriendlyString(config));
@@ -163,40 +166,21 @@
                         mSortedAudioPlaybackClientUids.add(0, uid);
                     }
                 }
-                // Notify the change of audio player states.
+                // Notify the active state change of audio players.
                 for (AudioPlaybackConfiguration config : configs) {
-                    final Integer prevState = prevAudioPlayerStates.get(config.getPlayerInterfaceId());
-                    final int prevStateInt =
-                            (prevState == null) ? AudioPlaybackConfiguration.PLAYER_STATE_UNKNOWN :
-                                prevState.intValue();
-                    if (prevStateInt != config.getPlayerState()) {
-                        sendAudioPlayerStateChangedMessageLocked(
-                                config.getClientUid(), prevStateInt, config);
+                    final int pii = config.getPlayerInterfaceId();
+                    boolean wasActive = mPrevActiveAudioPlaybackConfigs.remove(pii) != null;
+                    if (wasActive != config.isActive()) {
+                        sendAudioPlayerActiveStateChangedMessageLocked(
+                                config, /* isRemoved */ false);
                     }
                 }
-                for (Integer prevUid : prevAudioPlayersForUid.keySet()) {
-                    // If all players for prevUid is removed, notify the prev state was
-                    // PLAYER_STATE_STARTED only when there were a player whose state was
-                    // PLAYER_STATE_STARTED, otherwise any inactive state is okay to notify.
-                    if (!mAudioPlayersForUid.containsKey(prevUid)) {
-                        Set<Integer> prevPlayers = prevAudioPlayersForUid.get(prevUid);
-                        int prevState = AudioPlaybackConfiguration.PLAYER_STATE_UNKNOWN;
-                        for (int pii : prevPlayers) {
-                            Integer state = prevAudioPlayerStates.get(pii);
-                            if (state == null) {
-                                continue;
-                            }
-                            if (state == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
-                                prevState = state;
-                                break;
-                            } else if (prevState
-                                    == AudioPlaybackConfiguration.PLAYER_STATE_UNKNOWN) {
-                                prevState = state;
-                            }
-                        }
-                        sendAudioPlayerStateChangedMessageLocked(prevUid, prevState, null);
-                    }
+                for (AudioPlaybackConfiguration config : mPrevActiveAudioPlaybackConfigs.values()) {
+                    sendAudioPlayerActiveStateChangedMessageLocked(config, /* isRemoved */ true);
                 }
+
+                // Update mPrevActiveAudioPlaybackConfigs
+                mPrevActiveAudioPlaybackConfigs = activeAudioPlaybackConfigs;
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -204,9 +188,10 @@
     }
 
     /**
-     * Registers OnAudioPlayerStateChangedListener.
+     * Registers OnAudioPlayerActiveStateChangedListener.
      */
-    public void registerListener(OnAudioPlayerStateChangedListener listener, Handler handler) {
+    public void registerListener(
+            OnAudioPlayerActiveStateChangedListener listener, Handler handler) {
         synchronized (mLock) {
             mListenerMap.put(listener, new MessageHandler((handler == null) ?
                     Looper.myLooper() : handler.getLooper(), listener));
@@ -214,9 +199,9 @@
     }
 
     /**
-     * Unregisters OnAudioPlayerStateChangedListener.
+     * Unregisters OnAudioPlayerActiveStateChangedListener.
      */
-    public void unregisterListener(OnAudioPlayerStateChangedListener listener) {
+    public void unregisterListener(OnAudioPlayerActiveStateChangedListener listener) {
         synchronized (mLock) {
             mListenerMap.remove(listener);
         }
@@ -239,16 +224,7 @@
      */
     public boolean isPlaybackActive(int uid) {
         synchronized (mLock) {
-            Set<Integer> players = mAudioPlayersForUid.get(uid);
-            if (players == null) {
-                return false;
-            }
-            for (Integer pii : players) {
-                if (isActiveState(mAudioPlayerStates.get(pii))) {
-                    return true;
-                }
-            }
-            return false;
+            return mActiveAudioUids.contains(uid);
         }
     }
 
@@ -314,14 +290,10 @@
         }
     }
 
-    private void sendAudioPlayerStateChangedMessageLocked(
-            final int uid, final int prevState, final AudioPlaybackConfiguration config) {
+    private void sendAudioPlayerActiveStateChangedMessageLocked(
+            final AudioPlaybackConfiguration config, final boolean isRemoved) {
         for (MessageHandler messageHandler : mListenerMap.values()) {
-            messageHandler.sendAudioPlayerStateChangedMessage(uid, prevState, config);
+            messageHandler.sendAudioPlayerActiveStateChangedMessage(config, isRemoved);
         }
     }
-
-    private static boolean isActiveState(Integer state) {
-        return state != null && state.equals(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
-    }
 }
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 3c9e1d4..3e51252 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -19,7 +19,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.server.Watchdog;
 
-import android.annotation.Nullable;
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -101,6 +101,8 @@
     private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
     private final Handler mHandler = new Handler();
     private final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
+    private final IntArray mActivePlayerMinPriorityQueue = new IntArray();
+    private final IntArray mActivePlayerUidMinPriorityQueue = new IntArray();
 
     public MediaRouterService(Context context) {
         mContext = context;
@@ -111,7 +113,7 @@
 
         mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
         mAudioPlayerStateMonitor.registerListener(
-                new AudioPlayerStateMonitor.OnAudioPlayerStateChangedListener() {
+                new AudioPlayerStateMonitor.OnAudioPlayerActiveStateChangedListener() {
             static final long WAIT_MS = 500;
             final Runnable mRestoreBluetoothA2dpRunnable = new Runnable() {
                 @Override
@@ -121,39 +123,41 @@
             };
 
             @Override
-            public void onAudioPlayerStateChanged(
-                    int uid, int prevState, @Nullable AudioPlaybackConfiguration config) {
+            public void onAudioPlayerActiveStateChanged(
+                    @NonNull AudioPlaybackConfiguration config, boolean isRemoved) {
+                final boolean active = !isRemoved && config.isActive();
+                final int pii = config.getPlayerInterfaceId();
+                final int uid = config.getClientUid();
+
+                final int idx = mActivePlayerMinPriorityQueue.indexOf(pii);
+                // Keep the latest active player and its uid at the end of the queue.
+                if (idx >= 0) {
+                    mActivePlayerMinPriorityQueue.remove(idx);
+                    mActivePlayerUidMinPriorityQueue.remove(idx);
+                }
+
                 int restoreUid = -1;
-                boolean active = config == null ? false : config.isActive();
                 if (active) {
+                    mActivePlayerMinPriorityQueue.add(config.getPlayerInterfaceId());
+                    mActivePlayerUidMinPriorityQueue.add(uid);
                     restoreUid = uid;
-                } else if (prevState != AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
-                    // Noting to do if the prev state is not an active state.
-                    return;
-                } else {
-                    IntArray sortedAudioPlaybackClientUids =
-                            mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
-                    for (int i = 0; i < sortedAudioPlaybackClientUids.size(); ++i) {
-                        if (mAudioPlayerStateMonitor.isPlaybackActive(
-                                sortedAudioPlaybackClientUids.get(i))) {
-                            restoreUid = sortedAudioPlaybackClientUids.get(i);
-                            break;
-                        }
-                    }
+                } else if (mActivePlayerUidMinPriorityQueue.size() > 0) {
+                    restoreUid = mActivePlayerUidMinPriorityQueue.get(
+                            mActivePlayerUidMinPriorityQueue.size() - 1);
                 }
 
                 mHandler.removeCallbacks(mRestoreBluetoothA2dpRunnable);
                 if (restoreUid >= 0) {
                     restoreRoute(restoreUid);
                     if (DEBUG) {
-                        Slog.d(TAG, "onAudioPlayerStateChanged: " + "uid " + uid
-                                + " active " + active + " restoring " + restoreUid);
+                        Slog.d(TAG, "onAudioPlayerActiveStateChanged: " + "uid=" + uid
+                                + ", active=" + active + ", restoreUid=" + restoreUid);
                     }
                 } else {
                     mHandler.postDelayed(mRestoreBluetoothA2dpRunnable, WAIT_MS);
                     if (DEBUG) {
-                        Slog.d(TAG, "onAudioPlayerStateChanged: " + "uid " + uid
-                                + " active " + active + " delaying");
+                        Slog.d(TAG, "onAudioPlayerACTIVEStateChanged: " + "uid=" + uid
+                                + ", active=" + active + ", delaying");
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index f6a81d0..06f4f5e 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -16,7 +16,6 @@
 
 package com.android.server.media;
 
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.INotificationManager;
 import android.app.KeyguardManager;
@@ -138,23 +137,19 @@
         mAudioService = getAudioService();
         mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
         mAudioPlayerStateMonitor.registerListener(
-                new AudioPlayerStateMonitor.OnAudioPlayerStateChangedListener() {
-            @Override
-            public void onAudioPlayerStateChanged(
-                    int uid, int prevState, @Nullable AudioPlaybackConfiguration config) {
-                if (config == null || !config.isActive() || config.getPlayerType()
-                        == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
-                    return;
-                }
-                synchronized (mLock) {
-                    FullUserRecord user =
-                            getFullUserRecordLocked(UserHandle.getUserId(uid));
-                    if (user != null) {
-                        user.mPriorityStack.updateMediaButtonSessionIfNeeded();
+                (config, isRemoved) -> {
+                    if (isRemoved || !config.isActive() || config.getPlayerType()
+                            == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+                        return;
                     }
-                }
-            }
-        }, null /* handler */);
+                    synchronized (mLock) {
+                        FullUserRecord user = getFullUserRecordLocked(
+                                UserHandle.getUserId(config.getClientUid()));
+                        if (user != null) {
+                            user.mPriorityStack.updateMediaButtonSessionIfNeeded();
+                        }
+                    }
+                }, null /* handler */);
         mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService);
         mContentResolver = getContext().getContentResolver();
         mSettingsObserver = new SettingsObserver();