Add a class for managing Session priority

Priority is given first to the system priorty session, then to
active local sessions, then to active remote sessions, then to
the rest of the sessions. Ordering within categories is by whoever
last performed an action we associate with the user.

The stack has methods for getting filtered sets of this priority.

This also:
-Changes publish to setActive(boolean)
-Adds a flag for handling media buttons.
-Adds a flag for transport controls instead of enabling once.
-Unhides the setFlags API.
-Updates the legacy helper to use the flags.

Change-Id: I6ebeb27410de1b24149fd6e1785613ac444f0774
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
new file mode 100644
index 0000000..f9f004d
--- /dev/null
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2014 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.media;
+
+import android.media.session.PlaybackState;
+import android.media.session.Session;
+import android.text.TextUtils;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Keeps track of media sessions and their priority for notifications, media
+ * button routing, etc.
+ */
+public class MediaSessionStack {
+    /**
+     * These are states that usually indicate the user took an action and should
+     * bump priority regardless of the old state.
+     */
+    private static final int[] ALWAYS_PRIORITY_STATES = {
+            PlaybackState.PLAYSTATE_FAST_FORWARDING,
+            PlaybackState.PLAYSTATE_REWINDING,
+            PlaybackState.PLAYSTATE_SKIPPING_BACKWARDS,
+            PlaybackState.PLAYSTATE_SKIPPING_FORWARDS };
+    /**
+     * These are states that usually indicate the user took an action if they
+     * were entered from a non-priority state.
+     */
+    private static final int[] TRANSITION_PRIORITY_STATES = {
+            PlaybackState.PLAYSTATE_BUFFERING,
+            PlaybackState.PLAYSTATE_CONNECTING,
+            PlaybackState.PLAYSTATE_PLAYING };
+
+    private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
+
+    private MediaSessionRecord mCachedButtonReceiver;
+    private MediaSessionRecord mCachedDefault;
+    private ArrayList<MediaSessionRecord> mCachedActiveList;
+    private ArrayList<MediaSessionRecord> mCachedTransportControlList;
+
+    /**
+     * Add a record to the priority tracker.
+     *
+     * @param record The record to add.
+     */
+    public void addSession(MediaSessionRecord record) {
+        mSessions.add(record);
+        clearCache();
+    }
+
+    /**
+     * Remove a record from the priority tracker.
+     *
+     * @param record The record to remove.
+     */
+    public void removeSession(MediaSessionRecord record) {
+        mSessions.remove(record);
+        clearCache();
+    }
+
+    /**
+     * Notify the priority tracker that a session's state changed.
+     *
+     * @param record The record that changed.
+     * @param oldState Its old playback state.
+     * @param newState Its new playback state.
+     */
+    public void onPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
+        if (shouldUpdatePriority(oldState, newState)) {
+            mSessions.remove(record);
+            mSessions.add(0, record);
+            clearCache();
+        }
+    }
+
+    /**
+     * Handle any stack changes that need to occur in response to a session
+     * state change. TODO add the old and new session state as params
+     *
+     * @param record The record that changed.
+     */
+    public void onSessionStateChange(MediaSessionRecord record) {
+        // For now just clear the cache. Eventually we'll selectively clear
+        // depending on what changed.
+        clearCache();
+    }
+
+    /**
+     * Get the current priority sorted list of active sessions. The most
+     * important session is at index 0 and the least important at size - 1.
+     *
+     * @return All the active sessions in priority order.
+     */
+    public ArrayList<MediaSessionRecord> getActiveSessions() {
+        if (mCachedActiveList == null) {
+            mCachedActiveList = getPriorityListLocked(true, 0);
+        }
+        return mCachedActiveList;
+    }
+
+    /**
+     * Get the current priority sorted list of active sessions that use
+     * transport controls. The most important session is at index 0 and the
+     * least important at size -1.
+     *
+     * @return All the active sessions that handle transport controls in
+     *         priority order.
+     */
+    public ArrayList<MediaSessionRecord> getTransportControlSessions() {
+        if (mCachedTransportControlList == null) {
+            mCachedTransportControlList = getPriorityListLocked(true,
+                    Session.FLAG_HANDLES_TRANSPORT_CONTROLS);
+        }
+        return mCachedTransportControlList;
+    }
+
+    /**
+     * Get the highest priority active session.
+     *
+     * @return The current highest priority session or null.
+     */
+    public MediaSessionRecord getDefaultSession() {
+        if (mCachedDefault != null) {
+            return mCachedDefault;
+        }
+        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0);
+        if (records.size() > 0) {
+            return records.get(0);
+        }
+        return null;
+    }
+
+    /**
+     * Get the highest priority session that can handle media buttons.
+     *
+     * @return The default media button session or null.
+     */
+    public MediaSessionRecord getDefaultMediaButtonSession() {
+        if (mCachedButtonReceiver != null) {
+            return mCachedButtonReceiver;
+        }
+        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true,
+                Session.FLAG_HANDLES_MEDIA_BUTTONS);
+        if (records.size() > 0) {
+            mCachedButtonReceiver = records.get(0);
+        }
+        return mCachedButtonReceiver;
+    }
+
+    public void dumpLocked(PrintWriter pw, String prefix) {
+        ArrayList<MediaSessionRecord> sortedSessions = getPriorityListLocked(false, 0);
+        int count = sortedSessions.size();
+        pw.println(prefix + "Sessions Stack - have " + count + " sessions:");
+        String indent = prefix + "  ";
+        for (int i = 0; i < count; i++) {
+            MediaSessionRecord record = sortedSessions.get(i);
+            record.dump(pw, indent);
+            pw.println();
+        }
+    }
+
+    /**
+     * Get a priority sorted list of sessions. Can filter to only return active
+     * sessions or sessions with specific flags.
+     *
+     * @param activeOnly True to only return active sessions, false to return
+     *            all sessions.
+     * @param withFlags Only return sessions with all the specified flags set. 0
+     *            returns all sessions.
+     * @return The priority sorted list of sessions.
+     */
+    private ArrayList<MediaSessionRecord> getPriorityListLocked(boolean activeOnly, int withFlags) {
+        ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>();
+        int lastLocalIndex = 0;
+        int lastActiveIndex = 0;
+        int lastPublishedIndex = 0;
+
+        int size = mSessions.size();
+        for (int i = 0; i < size; i++) {
+            final MediaSessionRecord session = mSessions.get(i);
+
+            if ((session.getFlags() & withFlags) != withFlags) {
+                continue;
+            }
+            if (!session.isActive()) {
+                if (!activeOnly) {
+                    // If we're getting unpublished as well always put them at
+                    // the end
+                    result.add(session);
+                }
+                continue;
+            }
+
+            if (session.isSystemPriority()) {
+                // System priority sessions are special and always go at the
+                // front. We expect there to only be one of these at a time.
+                result.add(0, session);
+                lastLocalIndex++;
+                lastActiveIndex++;
+                lastPublishedIndex++;
+            } else if (session.isPlaybackActive()) {
+                // TODO replace getRoute() == null with real local route check
+                if(session.getRoute() == null) {
+                    // Active local sessions get top priority
+                    result.add(lastLocalIndex, session);
+                    lastLocalIndex++;
+                    lastActiveIndex++;
+                    lastPublishedIndex++;
+                } else {
+                    // Then active remote sessions
+                    result.add(lastActiveIndex, session);
+                    lastActiveIndex++;
+                    lastPublishedIndex++;
+                }
+            } else {
+                // inactive sessions go at the end in order of whoever last did
+                // something.
+                result.add(lastPublishedIndex, session);
+                lastPublishedIndex++;
+            }
+        }
+
+        return result;
+    }
+
+    private boolean shouldUpdatePriority(int oldState, int newState) {
+        if (containsState(newState, ALWAYS_PRIORITY_STATES)) {
+            return true;
+        }
+        if (!containsState(oldState, TRANSITION_PRIORITY_STATES)
+                && containsState(newState, TRANSITION_PRIORITY_STATES)) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean containsState(int state, int[] states) {
+        for (int i = 0; i < states.length; i++) {
+            if (states[i] == state) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void clearCache() {
+        mCachedDefault = null;
+        mCachedButtonReceiver = null;
+        mCachedActiveList = null;
+        mCachedTransportControlList = null;
+    }
+}