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