MediaSessionStack.java revision b69ffd4dc2c8fa85e0064151141ebeee90de471e
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.media;
18
19import android.media.session.PlaybackState;
20import android.media.session.MediaSession;
21import android.os.UserHandle;
22
23import java.io.PrintWriter;
24import java.util.ArrayList;
25
26/**
27 * Keeps track of media sessions and their priority for notifications, media
28 * button routing, etc.
29 */
30public class MediaSessionStack {
31    /**
32     * These are states that usually indicate the user took an action and should
33     * bump priority regardless of the old state.
34     */
35    private static final int[] ALWAYS_PRIORITY_STATES = {
36            PlaybackState.STATE_FAST_FORWARDING,
37            PlaybackState.STATE_REWINDING,
38            PlaybackState.STATE_SKIPPING_TO_PREVIOUS,
39            PlaybackState.STATE_SKIPPING_TO_NEXT };
40    /**
41     * These are states that usually indicate the user took an action if they
42     * were entered from a non-priority state.
43     */
44    private static final int[] TRANSITION_PRIORITY_STATES = {
45            PlaybackState.STATE_BUFFERING,
46            PlaybackState.STATE_CONNECTING,
47            PlaybackState.STATE_PLAYING };
48
49    private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
50
51    private MediaSessionRecord mGlobalPrioritySession;
52
53    private MediaSessionRecord mCachedButtonReceiver;
54    private MediaSessionRecord mCachedDefault;
55    private MediaSessionRecord mCachedVolumeDefault;
56    private ArrayList<MediaSessionRecord> mCachedActiveList;
57    private ArrayList<MediaSessionRecord> mCachedTransportControlList;
58
59    /**
60     * Add a record to the priority tracker.
61     *
62     * @param record The record to add.
63     */
64    public void addSession(MediaSessionRecord record) {
65        mSessions.add(record);
66        if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
67            mGlobalPrioritySession = record;
68        }
69        clearCache();
70    }
71
72    /**
73     * Remove a record from the priority tracker.
74     *
75     * @param record The record to remove.
76     */
77    public void removeSession(MediaSessionRecord record) {
78        mSessions.remove(record);
79        if (record == mGlobalPrioritySession) {
80            mGlobalPrioritySession = null;
81        }
82        clearCache();
83    }
84
85    /**
86     * Notify the priority tracker that a session's state changed.
87     *
88     * @param record The record that changed.
89     * @param oldState Its old playback state.
90     * @param newState Its new playback state.
91     */
92    public void onPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
93        if (shouldUpdatePriority(oldState, newState)) {
94            mSessions.remove(record);
95            mSessions.add(0, record);
96            clearCache();
97        } else if (newState == PlaybackState.STATE_PAUSED) {
98            // Just clear the volume cache in this case
99            mCachedVolumeDefault = null;
100        }
101    }
102
103    /**
104     * Handle any stack changes that need to occur in response to a session
105     * state change. TODO add the old and new session state as params
106     *
107     * @param record The record that changed.
108     */
109    public void onSessionStateChange(MediaSessionRecord record) {
110        // For now just clear the cache. Eventually we'll selectively clear
111        // depending on what changed.
112        clearCache();
113    }
114
115    /**
116     * Get the current priority sorted list of active sessions. The most
117     * important session is at index 0 and the least important at size - 1.
118     *
119     * @param userId The user to check.
120     * @return All the active sessions in priority order.
121     */
122    public ArrayList<MediaSessionRecord> getActiveSessions(int userId) {
123        if (mCachedActiveList == null) {
124            mCachedActiveList = getPriorityListLocked(true, 0, userId);
125        }
126        return mCachedActiveList;
127    }
128
129    /**
130     * Get the current priority sorted list of active sessions that use
131     * transport controls. The most important session is at index 0 and the
132     * least important at size -1.
133     *
134     * @param userId The user to check.
135     * @return All the active sessions that handle transport controls in
136     *         priority order.
137     */
138    public ArrayList<MediaSessionRecord> getTransportControlSessions(int userId) {
139        if (mCachedTransportControlList == null) {
140            mCachedTransportControlList = getPriorityListLocked(true,
141                    MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS, userId);
142        }
143        return mCachedTransportControlList;
144    }
145
146    /**
147     * Get the highest priority active session.
148     *
149     * @param userId The user to check.
150     * @return The current highest priority session or null.
151     */
152    public MediaSessionRecord getDefaultSession(int userId) {
153        if (mCachedDefault != null) {
154            return mCachedDefault;
155        }
156        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId);
157        if (records.size() > 0) {
158            return records.get(0);
159        }
160        return null;
161    }
162
163    /**
164     * Get the highest priority session that can handle media buttons.
165     *
166     * @param userId The user to check.
167     * @return The default media button session or null.
168     */
169    public MediaSessionRecord getDefaultMediaButtonSession(int userId) {
170        if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) {
171            return mGlobalPrioritySession;
172        }
173        if (mCachedButtonReceiver != null) {
174            return mCachedButtonReceiver;
175        }
176        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true,
177                MediaSession.FLAG_HANDLES_MEDIA_BUTTONS, userId);
178        if (records.size() > 0) {
179            mCachedButtonReceiver = records.get(0);
180        }
181        return mCachedButtonReceiver;
182    }
183
184    public MediaSessionRecord getDefaultVolumeSession(int userId) {
185        if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) {
186            return mGlobalPrioritySession;
187        }
188        if (mCachedVolumeDefault != null) {
189            return mCachedVolumeDefault;
190        }
191        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId);
192        int size = records.size();
193        for (int i = 0; i < size; i++) {
194            MediaSessionRecord record = records.get(i);
195            if (record.isPlaybackActive(false)) {
196                mCachedVolumeDefault = record;
197                return record;
198            }
199        }
200        return null;
201    }
202
203    public void dump(PrintWriter pw, String prefix) {
204        ArrayList<MediaSessionRecord> sortedSessions = getPriorityListLocked(false, 0,
205                UserHandle.USER_ALL);
206        int count = sortedSessions.size();
207        pw.println(prefix + "Sessions Stack - have " + count + " sessions:");
208        String indent = prefix + "  ";
209        for (int i = 0; i < count; i++) {
210            MediaSessionRecord record = sortedSessions.get(i);
211            record.dump(pw, indent);
212            pw.println();
213        }
214    }
215
216    /**
217     * Get a priority sorted list of sessions. Can filter to only return active
218     * sessions or sessions with specific flags.
219     *
220     * @param activeOnly True to only return active sessions, false to return
221     *            all sessions.
222     * @param withFlags Only return sessions with all the specified flags set. 0
223     *            returns all sessions.
224     * @param userId The user to get sessions for. {@link UserHandle#USER_ALL}
225     *            will return sessions for all users.
226     * @return The priority sorted list of sessions.
227     */
228    private ArrayList<MediaSessionRecord> getPriorityListLocked(boolean activeOnly, int withFlags,
229            int userId) {
230        ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>();
231        int lastLocalIndex = 0;
232        int lastActiveIndex = 0;
233        int lastPublishedIndex = 0;
234
235        int size = mSessions.size();
236        for (int i = 0; i < size; i++) {
237            final MediaSessionRecord session = mSessions.get(i);
238
239            if (userId != UserHandle.USER_ALL && userId != session.getUserId()) {
240                // Filter out sessions for the wrong user
241                continue;
242            }
243            if ((session.getFlags() & withFlags) != withFlags) {
244                // Filter out sessions with the wrong flags
245                continue;
246            }
247            if (!session.isActive()) {
248                if (!activeOnly) {
249                    // If we're getting unpublished as well always put them at
250                    // the end
251                    result.add(session);
252                }
253                continue;
254            }
255
256            if (session.isSystemPriority()) {
257                // System priority sessions are special and always go at the
258                // front. We expect there to only be one of these at a time.
259                result.add(0, session);
260                lastLocalIndex++;
261                lastActiveIndex++;
262                lastPublishedIndex++;
263            } else if (session.isPlaybackActive(true)) {
264                // TODO replace getRoute() == null with real local route check
265                if(session.getRoute() == null) {
266                    // Active local sessions get top priority
267                    result.add(lastLocalIndex, session);
268                    lastLocalIndex++;
269                    lastActiveIndex++;
270                    lastPublishedIndex++;
271                } else {
272                    // Then active remote sessions
273                    result.add(lastActiveIndex, session);
274                    lastActiveIndex++;
275                    lastPublishedIndex++;
276                }
277            } else {
278                // inactive sessions go at the end in order of whoever last did
279                // something.
280                result.add(lastPublishedIndex, session);
281                lastPublishedIndex++;
282            }
283        }
284
285        return result;
286    }
287
288    private boolean shouldUpdatePriority(int oldState, int newState) {
289        if (containsState(newState, ALWAYS_PRIORITY_STATES)) {
290            return true;
291        }
292        if (!containsState(oldState, TRANSITION_PRIORITY_STATES)
293                && containsState(newState, TRANSITION_PRIORITY_STATES)) {
294            return true;
295        }
296        return false;
297    }
298
299    private boolean containsState(int state, int[] states) {
300        for (int i = 0; i < states.length; i++) {
301            if (states[i] == state) {
302                return true;
303            }
304        }
305        return false;
306    }
307
308    private void clearCache() {
309        mCachedDefault = null;
310        mCachedVolumeDefault = null;
311        mCachedButtonReceiver = null;
312        mCachedActiveList = null;
313        mCachedTransportControlList = null;
314    }
315}
316