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