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