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.MediaSession;
21import android.media.session.PlaybackState;
22import android.os.Debug;
23import android.os.UserHandle;
24import android.util.IntArray;
25import android.util.Log;
26import android.util.SparseArray;
27
28import java.io.PrintWriter;
29import java.util.ArrayList;
30import java.util.List;
31
32/**
33 * Keeps track of media sessions and their priority for notifications, media
34 * button dispatch, etc.
35 * <p>This class isn't thread-safe. The caller should take care of the synchronization.
36 */
37class MediaSessionStack {
38    private static final boolean DEBUG = MediaSessionService.DEBUG;
39    private static final String TAG = "MediaSessionStack";
40
41    /**
42     * Listen the change in the media button session.
43     */
44    interface OnMediaButtonSessionChangedListener {
45        /**
46         * Called when the media button session is changed.
47         */
48        void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession,
49                MediaSessionRecord newMediaButtonSession);
50    }
51
52    /**
53     * These are states that usually indicate the user took an action and should
54     * bump priority regardless of the old state.
55     */
56    private static final int[] ALWAYS_PRIORITY_STATES = {
57            PlaybackState.STATE_FAST_FORWARDING,
58            PlaybackState.STATE_REWINDING,
59            PlaybackState.STATE_SKIPPING_TO_PREVIOUS,
60            PlaybackState.STATE_SKIPPING_TO_NEXT };
61    /**
62     * These are states that usually indicate the user took an action if they
63     * were entered from a non-priority state.
64     */
65    private static final int[] TRANSITION_PRIORITY_STATES = {
66            PlaybackState.STATE_BUFFERING,
67            PlaybackState.STATE_CONNECTING,
68            PlaybackState.STATE_PLAYING };
69
70    /**
71     * Sorted list of the media sessions.
72     * The session of which PlaybackState is changed to ALWAYS_PRIORITY_STATES or
73     * TRANSITION_PRIORITY_STATES comes first.
74     * @see #shouldUpdatePriority
75     */
76    private final List<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
77
78    private final AudioPlaybackMonitor mAudioPlaybackMonitor;
79    private final OnMediaButtonSessionChangedListener mOnMediaButtonSessionChangedListener;
80
81    /**
82     * The media button session which receives media key events.
83     * It could be null if the previous media buttion session is released.
84     */
85    private MediaSessionRecord mMediaButtonSession;
86
87    private MediaSessionRecord mCachedDefault;
88    private MediaSessionRecord mCachedVolumeDefault;
89
90    /**
91     * Cache the result of the {@link #getActiveSessions} per user.
92     */
93    private final SparseArray<ArrayList<MediaSessionRecord>> mCachedActiveLists =
94            new SparseArray<>();
95
96    MediaSessionStack(AudioPlaybackMonitor monitor, OnMediaButtonSessionChangedListener listener) {
97        mAudioPlaybackMonitor = monitor;
98        mOnMediaButtonSessionChangedListener = listener;
99    }
100
101    /**
102     * Add a record to the priority tracker.
103     *
104     * @param record The record to add.
105     */
106    public void addSession(MediaSessionRecord record) {
107        mSessions.add(record);
108        clearCache(record.getUserId());
109
110        // Update the media button session.
111        // The added session could be the session from the package with the audio playback.
112        // This can happen if an app starts audio playback before creating media session.
113        updateMediaButtonSessionIfNeeded();
114    }
115
116    /**
117     * Remove a record from the priority tracker.
118     *
119     * @param record The record to remove.
120     */
121    public void removeSession(MediaSessionRecord record) {
122        mSessions.remove(record);
123        if (mMediaButtonSession == record) {
124            // When the media button session is removed, nullify the media button session and do not
125            // search for the alternative media session within the app. It's because the alternative
126            // media session might be a dummy which isn't able to handle the media key events.
127            updateMediaButtonSession(null);
128        }
129        clearCache(record.getUserId());
130    }
131
132    /**
133     * Return if the record exists in the priority tracker.
134     */
135    public boolean contains(MediaSessionRecord record) {
136        return mSessions.contains(record);
137    }
138
139    /**
140     * Notify the priority tracker that a session's playback state changed.
141     *
142     * @param record The record that changed.
143     * @param oldState Its old playback state.
144     * @param newState Its new playback state.
145     */
146    public void onPlaystateChanged(MediaSessionRecord record, int oldState, int newState) {
147        if (shouldUpdatePriority(oldState, newState)) {
148            mSessions.remove(record);
149            mSessions.add(0, record);
150            clearCache(record.getUserId());
151        } else if (!MediaSession.isActiveState(newState)) {
152            // Just clear the volume cache when a state goes inactive
153            mCachedVolumeDefault = null;
154        }
155
156        // In most cases, playback state isn't needed for finding media button session,
157        // but we only use it as a hint if an app has multiple local media sessions.
158        // In that case, we pick the media session whose PlaybackState matches
159        // the audio playback configuration.
160        if (mMediaButtonSession != null && mMediaButtonSession.getUid() == record.getUid()) {
161            MediaSessionRecord newMediaButtonSession =
162                    findMediaButtonSession(mMediaButtonSession.getUid());
163            if (newMediaButtonSession != mMediaButtonSession) {
164                updateMediaButtonSession(newMediaButtonSession);
165            }
166        }
167    }
168
169    /**
170     * Handle the change in activeness for a session.
171     *
172     * @param record The record that changed.
173     */
174    public void onSessionStateChange(MediaSessionRecord record) {
175        // For now just clear the cache. Eventually we'll selectively clear
176        // depending on what changed.
177        clearCache(record.getUserId());
178    }
179
180    /**
181     * Update the media button session if needed.
182     * <p>The media button session is the session that will receive the media button events.
183     * <p>We send the media button events to the lastly played app. If the app has the media
184     * session, the session will receive the media button events.
185     */
186    public void updateMediaButtonSessionIfNeeded() {
187        if (DEBUG) {
188            Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + Debug.getCallers(2));
189        }
190        IntArray audioPlaybackUids = mAudioPlaybackMonitor.getSortedAudioPlaybackClientUids();
191        for (int i = 0; i < audioPlaybackUids.size(); i++) {
192            MediaSessionRecord mediaButtonSession =
193                    findMediaButtonSession(audioPlaybackUids.get(i));
194            if (mediaButtonSession != null) {
195                // Found the media button session.
196                mAudioPlaybackMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
197                if (mMediaButtonSession != mediaButtonSession) {
198                    updateMediaButtonSession(mediaButtonSession);
199                }
200                return;
201            }
202        }
203    }
204
205    /**
206     * Find the media button session with the given {@param uid}.
207     * If the app has multiple media sessions, the media session whose playback state is not null
208     * and matches the audio playback state becomes the media button session. Otherwise the top
209     * priority session becomes the media button session.
210     *
211     * @return The media button session. Returns {@code null} if the app doesn't have a media
212     *   session.
213     */
214    private MediaSessionRecord findMediaButtonSession(int uid) {
215        MediaSessionRecord mediaButtonSession = null;
216        for (MediaSessionRecord session : mSessions) {
217            if (uid == session.getUid()) {
218                if (session.getPlaybackState() != null && session.isPlaybackActive() ==
219                        mAudioPlaybackMonitor.isPlaybackActive(session.getUid())) {
220                    // If there's a media session whose PlaybackState matches
221                    // the audio playback state, return it immediately.
222                    return session;
223                }
224                if (mediaButtonSession == null) {
225                    // Among the media sessions whose PlaybackState doesn't match
226                    // the audio playback state, pick the top priority.
227                    mediaButtonSession = session;
228                }
229            }
230        }
231        return mediaButtonSession;
232    }
233
234    /**
235     * Get the current priority sorted list of active sessions. The most
236     * important session is at index 0 and the least important at size - 1.
237     *
238     * @param userId The user to check. It can be {@link UserHandle#USER_ALL} to get all sessions
239     *    for all users in this {@link MediaSessionStack}.
240     * @return All the active sessions in priority order.
241     */
242    public ArrayList<MediaSessionRecord> getActiveSessions(int userId) {
243        ArrayList<MediaSessionRecord> cachedActiveList = mCachedActiveLists.get(userId);
244        if (cachedActiveList == null) {
245            cachedActiveList = getPriorityList(true, userId);
246            mCachedActiveLists.put(userId, cachedActiveList);
247        }
248        return cachedActiveList;
249    }
250
251    /**
252     * Get the media button session which receives the media button events.
253     *
254     * @return The media button session or null.
255     */
256    public MediaSessionRecord getMediaButtonSession() {
257        return mMediaButtonSession;
258    }
259
260    private void updateMediaButtonSession(MediaSessionRecord newMediaButtonSession) {
261        MediaSessionRecord oldMediaButtonSession = mMediaButtonSession;
262        mMediaButtonSession = newMediaButtonSession;
263        mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(
264                oldMediaButtonSession, newMediaButtonSession);
265    }
266
267    public MediaSessionRecord getDefaultVolumeSession() {
268        if (mCachedVolumeDefault != null) {
269            return mCachedVolumeDefault;
270        }
271        ArrayList<MediaSessionRecord> records = getPriorityList(true, UserHandle.USER_ALL);
272        int size = records.size();
273        for (int i = 0; i < size; i++) {
274            MediaSessionRecord record = records.get(i);
275            if (record.isPlaybackActive()) {
276                mCachedVolumeDefault = record;
277                return record;
278            }
279        }
280        return null;
281    }
282
283    public MediaSessionRecord getDefaultRemoteSession(int userId) {
284        ArrayList<MediaSessionRecord> records = getPriorityList(true, userId);
285
286        int size = records.size();
287        for (int i = 0; i < size; i++) {
288            MediaSessionRecord record = records.get(i);
289            if (record.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
290                return record;
291            }
292        }
293        return null;
294    }
295
296    public void dump(PrintWriter pw, String prefix) {
297        ArrayList<MediaSessionRecord> sortedSessions = getPriorityList(false,
298                UserHandle.USER_ALL);
299        int count = sortedSessions.size();
300        pw.println(prefix + "Media button session is " + mMediaButtonSession);
301        pw.println(prefix + "Sessions Stack - have " + count + " sessions:");
302        String indent = prefix + "  ";
303        for (int i = 0; i < count; i++) {
304            MediaSessionRecord record = sortedSessions.get(i);
305            record.dump(pw, indent);
306            pw.println();
307        }
308    }
309
310    /**
311     * Get a priority sorted list of sessions. Can filter to only return active
312     * sessions or sessions.
313     * <p>Here's the priority order.
314     * <li>Active sessions whose PlaybackState is active</li>
315     * <li>Active sessions whose PlaybackState is inactive</li>
316     * <li>Inactive sessions</li>
317     *
318     * @param activeOnly True to only return active sessions, false to return
319     *            all sessions.
320     * @param userId The user to get sessions for. {@link UserHandle#USER_ALL}
321     *            will return sessions for all users.
322     * @return The priority sorted list of sessions.
323     */
324    public ArrayList<MediaSessionRecord> getPriorityList(boolean activeOnly, int userId) {
325        ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>();
326        int lastPlaybackActiveIndex = 0;
327        int lastActiveIndex = 0;
328
329        int size = mSessions.size();
330        for (int i = 0; i < size; i++) {
331            final MediaSessionRecord session = mSessions.get(i);
332
333            if (userId != UserHandle.USER_ALL && userId != session.getUserId()) {
334                // Filter out sessions for the wrong user
335                continue;
336            }
337
338            if (!session.isActive()) {
339                if (!activeOnly) {
340                    // If we're getting unpublished as well always put them at
341                    // the end
342                    result.add(session);
343                }
344                continue;
345            }
346
347            if (session.isPlaybackActive()) {
348                result.add(lastPlaybackActiveIndex++, session);
349                lastActiveIndex++;
350            } else {
351                result.add(lastActiveIndex++, session);
352            }
353        }
354
355        return result;
356    }
357
358    private boolean shouldUpdatePriority(int oldState, int newState) {
359        if (containsState(newState, ALWAYS_PRIORITY_STATES)) {
360            return true;
361        }
362        if (!containsState(oldState, TRANSITION_PRIORITY_STATES)
363                && containsState(newState, TRANSITION_PRIORITY_STATES)) {
364            return true;
365        }
366        return false;
367    }
368
369    private boolean containsState(int state, int[] states) {
370        for (int i = 0; i < states.length; i++) {
371            if (states[i] == state) {
372                return true;
373            }
374        }
375        return false;
376    }
377
378    private void clearCache(int userId) {
379        mCachedDefault = null;
380        mCachedVolumeDefault = null;
381        mCachedActiveLists.remove(userId);
382        // mCachedActiveLists may also include the list of sessions for UserHandle.USER_ALL,
383        // so they also need to be cleared.
384        mCachedActiveLists.remove(UserHandle.USER_ALL);
385    }
386}
387