MediaSessionStack.java revision 4d26598677095e74e30fc12f46a769cac800f946
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 dispatch, 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        clearCache();
67    }
68
69    /**
70     * Remove a record from the priority tracker.
71     *
72     * @param record The record to remove.
73     */
74    public void removeSession(MediaSessionRecord record) {
75        mSessions.remove(record);
76        if (record == mGlobalPrioritySession) {
77            mGlobalPrioritySession = null;
78        }
79        clearCache();
80    }
81
82    /**
83     * Notify the priority tracker that a session's state changed.
84     *
85     * @param record The record that changed.
86     * @param oldState Its old playback state.
87     * @param newState Its new playback state.
88     * @return true if the priority order was updated, false otherwise.
89     */
90    public boolean onPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
91        if (shouldUpdatePriority(oldState, newState)) {
92            mSessions.remove(record);
93            mSessions.add(0, record);
94            clearCache();
95            return true;
96        } else if (newState == PlaybackState.STATE_PAUSED) {
97            // Just clear the volume cache in this case
98            mCachedVolumeDefault = null;
99        }
100        return false;
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        if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
111            mGlobalPrioritySession = record;
112        }
113        // For now just clear the cache. Eventually we'll selectively clear
114        // depending on what changed.
115        clearCache();
116    }
117
118    /**
119     * Get the current priority sorted list of active sessions. The most
120     * important session is at index 0 and the least important at size - 1.
121     *
122     * @param userId The user to check.
123     * @return All the active sessions in priority order.
124     */
125    public ArrayList<MediaSessionRecord> getActiveSessions(int userId) {
126        if (mCachedActiveList == null) {
127            mCachedActiveList = getPriorityListLocked(true, 0, userId);
128        }
129        return mCachedActiveList;
130    }
131
132    /**
133     * Get the current priority sorted list of active sessions that use
134     * transport controls. The most important session is at index 0 and the
135     * least important at size -1.
136     *
137     * @param userId The user to check.
138     * @return All the active sessions that handle transport controls in
139     *         priority order.
140     */
141    public ArrayList<MediaSessionRecord> getTransportControlSessions(int userId) {
142        if (mCachedTransportControlList == null) {
143            mCachedTransportControlList = getPriorityListLocked(true,
144                    MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS, userId);
145        }
146        return mCachedTransportControlList;
147    }
148
149    /**
150     * Get the highest priority active session.
151     *
152     * @param userId The user to check.
153     * @return The current highest priority session or null.
154     */
155    public MediaSessionRecord getDefaultSession(int userId) {
156        if (mCachedDefault != null) {
157            return mCachedDefault;
158        }
159        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId);
160        if (records.size() > 0) {
161            return records.get(0);
162        }
163        return null;
164    }
165
166    /**
167     * Get the highest priority session that can handle media buttons.
168     *
169     * @param userId The user to check.
170     * @return The default media button session or null.
171     */
172    public MediaSessionRecord getDefaultMediaButtonSession(int userId) {
173        if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) {
174            return mGlobalPrioritySession;
175        }
176        if (mCachedButtonReceiver != null) {
177            return mCachedButtonReceiver;
178        }
179        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true,
180                MediaSession.FLAG_HANDLES_MEDIA_BUTTONS, userId);
181        if (records.size() > 0) {
182            mCachedButtonReceiver = records.get(0);
183        }
184        return mCachedButtonReceiver;
185    }
186
187    public MediaSessionRecord getDefaultVolumeSession(int userId) {
188        if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) {
189            return mGlobalPrioritySession;
190        }
191        if (mCachedVolumeDefault != null) {
192            return mCachedVolumeDefault;
193        }
194        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId);
195        int size = records.size();
196        for (int i = 0; i < size; i++) {
197            MediaSessionRecord record = records.get(i);
198            if (record.isPlaybackActive(false)) {
199                mCachedVolumeDefault = record;
200                return record;
201            }
202        }
203        return null;
204    }
205
206    public MediaSessionRecord getDefaultRemoteSession(int userId) {
207        ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId);
208
209        int size = records.size();
210        for (int i = 0; i < size; i++) {
211            MediaSessionRecord record = records.get(i);
212            if (record.getPlaybackType() == MediaSession.PLAYBACK_TYPE_REMOTE) {
213                return record;
214            }
215        }
216        return null;
217    }
218
219    public void dump(PrintWriter pw, String prefix) {
220        ArrayList<MediaSessionRecord> sortedSessions = getPriorityListLocked(false, 0,
221                UserHandle.USER_ALL);
222        int count = sortedSessions.size();
223        pw.println(prefix + "Global priority session is " + mGlobalPrioritySession);
224        pw.println(prefix + "Sessions Stack - have " + count + " sessions:");
225        String indent = prefix + "  ";
226        for (int i = 0; i < count; i++) {
227            MediaSessionRecord record = sortedSessions.get(i);
228            record.dump(pw, indent);
229            pw.println();
230        }
231    }
232
233    /**
234     * Get a priority sorted list of sessions. Can filter to only return active
235     * sessions or sessions with specific flags.
236     *
237     * @param activeOnly True to only return active sessions, false to return
238     *            all sessions.
239     * @param withFlags Only return sessions with all the specified flags set. 0
240     *            returns all sessions.
241     * @param userId The user to get sessions for. {@link UserHandle#USER_ALL}
242     *            will return sessions for all users.
243     * @return The priority sorted list of sessions.
244     */
245    private ArrayList<MediaSessionRecord> getPriorityListLocked(boolean activeOnly, int withFlags,
246            int userId) {
247        ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>();
248        int lastLocalIndex = 0;
249        int lastActiveIndex = 0;
250        int lastPublishedIndex = 0;
251
252        int size = mSessions.size();
253        for (int i = 0; i < size; i++) {
254            final MediaSessionRecord session = mSessions.get(i);
255
256            if (userId != UserHandle.USER_ALL && userId != session.getUserId()) {
257                // Filter out sessions for the wrong user
258                continue;
259            }
260            if ((session.getFlags() & withFlags) != withFlags) {
261                // Filter out sessions with the wrong flags
262                continue;
263            }
264            if (!session.isActive()) {
265                if (!activeOnly) {
266                    // If we're getting unpublished as well always put them at
267                    // the end
268                    result.add(session);
269                }
270                continue;
271            }
272
273            if (session.isSystemPriority()) {
274                // System priority sessions are special and always go at the
275                // front. We expect there to only be one of these at a time.
276                result.add(0, session);
277                lastLocalIndex++;
278                lastActiveIndex++;
279                lastPublishedIndex++;
280            } else if (session.isPlaybackActive(true)) {
281                // TODO this with real local route check
282                if (true) {
283                    // Active local sessions get top priority
284                    result.add(lastLocalIndex, session);
285                    lastLocalIndex++;
286                    lastActiveIndex++;
287                    lastPublishedIndex++;
288                } else {
289                    // Then active remote sessions
290                    result.add(lastActiveIndex, session);
291                    lastActiveIndex++;
292                    lastPublishedIndex++;
293                }
294            } else {
295                // inactive sessions go at the end in order of whoever last did
296                // something.
297                result.add(lastPublishedIndex, session);
298                lastPublishedIndex++;
299            }
300        }
301
302        return result;
303    }
304
305    private boolean shouldUpdatePriority(int oldState, int newState) {
306        if (containsState(newState, ALWAYS_PRIORITY_STATES)) {
307            return true;
308        }
309        if (!containsState(oldState, TRANSITION_PRIORITY_STATES)
310                && containsState(newState, TRANSITION_PRIORITY_STATES)) {
311            return true;
312        }
313        return false;
314    }
315
316    private boolean containsState(int state, int[] states) {
317        for (int i = 0; i < states.length; i++) {
318            if (states[i] == state) {
319                return true;
320            }
321        }
322        return false;
323    }
324
325    private void clearCache() {
326        mCachedDefault = null;
327        mCachedVolumeDefault = null;
328        mCachedButtonReceiver = null;
329        mCachedActiveList = null;
330        mCachedTransportControlList = null;
331    }
332}
333