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 
17 package com.android.server.media;
18 
19 import static com.android.server.media.MediaSessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_SESSION;
20 
21 import android.media.Session2Token;
22 import android.media.session.MediaSession;
23 import android.os.UserHandle;
24 import android.text.TextUtils;
25 import android.util.Log;
26 import android.util.Slog;
27 import android.util.SparseArray;
28 
29 import java.io.PrintWriter;
30 import java.util.ArrayList;
31 import java.util.List;
32 import java.util.Objects;
33 
34 /**
35  * Keeps track of media sessions and their priority for notifications, media
36  * button dispatch, etc.
37  * <p>This class isn't thread-safe. The caller should take care of the synchronization.
38  */
39 class MediaSessionStack {
40     private static final boolean DEBUG = MediaSessionService.DEBUG;
41     private static final String TAG = "MediaSessionStack";
42 
43     private static final int DUMP_EVENTS_MAX_COUNT = 70;
44 
45     /**
46      * Listen the change in the media button session.
47      */
48     interface OnMediaButtonSessionChangedListener {
49         /**
50          * Called when the media button session is changed.
51          */
onMediaButtonSessionChanged(MediaSessionRecordImpl oldMediaButtonSession, MediaSessionRecordImpl newMediaButtonSession)52         void onMediaButtonSessionChanged(MediaSessionRecordImpl oldMediaButtonSession,
53                 MediaSessionRecordImpl newMediaButtonSession);
54     }
55 
56     /**
57      * Sorted list of the media sessions
58      */
59     private final List<MediaSessionRecordImpl> mSessions = new ArrayList<>();
60 
61     private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
62     private final OnMediaButtonSessionChangedListener mOnMediaButtonSessionChangedListener;
63 
64     /**
65      * The media button session which receives media key events.
66      * It could be null if the previous media button session is released.
67      */
68     private MediaSessionRecordImpl mMediaButtonSession;
69 
70     /**
71      * Cache the result of the {@link #getActiveSessions} per user.
72      */
73     private final SparseArray<List<MediaSessionRecord>> mCachedActiveLists =
74             new SparseArray<>();
75 
MediaSessionStack(AudioPlayerStateMonitor monitor, OnMediaButtonSessionChangedListener listener)76     MediaSessionStack(AudioPlayerStateMonitor monitor, OnMediaButtonSessionChangedListener listener) {
77         mAudioPlayerStateMonitor = monitor;
78         mOnMediaButtonSessionChangedListener = listener;
79     }
80 
81     /**
82      * Add a record to the priority tracker.
83      *
84      * @param record The record to add.
85      */
addSession(MediaSessionRecordImpl record)86     public void addSession(MediaSessionRecordImpl record) {
87         Slog.i(TAG, TextUtils.formatSimple(
88                 "addSession to bottom of stack | record: %s",
89                 record
90         ));
91         mSessions.add(record);
92         clearCache(record.getUserId());
93 
94         // Update the media button session.
95         // The added session could be the session from the package with the audio playback.
96         // This can happen if an app starts audio playback before creating media session.
97         updateMediaButtonSessionIfNeeded();
98     }
99 
100     /**
101      * Remove a record from the priority tracker.
102      *
103      * @param record The record to remove.
104      */
removeSession(MediaSessionRecordImpl record)105     public void removeSession(MediaSessionRecordImpl record) {
106         Slog.i(TAG, TextUtils.formatSimple(
107                 "removeSession | record: %s",
108                 record
109         ));
110         mSessions.remove(record);
111         if (mMediaButtonSession == record) {
112             // When the media button session is removed, nullify the media button session and do not
113             // search for the alternative media session within the app. It's because the alternative
114             // media session might be a fake which isn't able to handle the media key events.
115             // TODO(b/154456172): Make this decision unaltered by non-media app's playback.
116             updateMediaButtonSession(null);
117         }
118         clearCache(record.getUserId());
119     }
120 
121     /**
122      * Return if the record exists in the priority tracker.
123      */
contains(MediaSessionRecordImpl record)124     public boolean contains(MediaSessionRecordImpl record) {
125         return mSessions.contains(record);
126     }
127 
128     /**
129      * Gets the {@link MediaSessionRecord} with the {@link MediaSession.Token}.
130      *
131      * @param sessionToken session token
132      * @return the MediaSessionRecord. Can be {@code null} if the session is gone meanwhile.
133      */
getMediaSessionRecord(MediaSession.Token sessionToken)134     public MediaSessionRecord getMediaSessionRecord(MediaSession.Token sessionToken) {
135         for (MediaSessionRecordImpl record : mSessions) {
136             if (record instanceof MediaSessionRecord) {
137                 MediaSessionRecord session1 = (MediaSessionRecord) record;
138                 if (Objects.equals(session1.getSessionToken(), sessionToken)) {
139                     return session1;
140                 }
141             }
142         }
143         return null;
144     }
145 
146     /**
147      * Notify the priority tracker that a session's playback state changed.
148      *
149      * @param record The record that changed.
150      * @param shouldUpdatePriority {@code true} if the record needs to prioritized
151      */
onPlaybackStateChanged( MediaSessionRecordImpl record, boolean shouldUpdatePriority)152     public void onPlaybackStateChanged(
153             MediaSessionRecordImpl record, boolean shouldUpdatePriority) {
154         if (shouldUpdatePriority) {
155             Slog.i(TAG, TextUtils.formatSimple(
156                     "onPlaybackStateChanged - Pushing session to top | record: %s",
157                     record
158             ));
159             mSessions.remove(record);
160             mSessions.add(0, record);
161             clearCache(record.getUserId());
162         }
163 
164         // In most cases, playback state isn't needed for finding media button session,
165         // but we only use it as a hint if an app has multiple local media sessions.
166         // In that case, we pick the media session whose PlaybackState matches
167         // the audio playback configuration.
168         if (mMediaButtonSession != null && mMediaButtonSession.getUid() == record.getUid()) {
169             MediaSessionRecordImpl newMediaButtonSession =
170                     findMediaButtonSession(mMediaButtonSession.getUid());
171             if (newMediaButtonSession != mMediaButtonSession
172                     && (newMediaButtonSession.getSessionPolicies()
173                             & SESSION_POLICY_IGNORE_BUTTON_SESSION) == 0) {
174                 // Check if the policy states that this session should not be updated as a media
175                 // button session.
176                 updateMediaButtonSession(newMediaButtonSession);
177             }
178         }
179     }
180 
181     /**
182      * Handle the change in activeness for a session.
183      *
184      * @param record The record that changed.
185      */
onSessionActiveStateChanged(MediaSessionRecordImpl record)186     public void onSessionActiveStateChanged(MediaSessionRecordImpl record) {
187         // For now just clear the cache. Eventually we'll selectively clear
188         // depending on what changed.
189         clearCache(record.getUserId());
190     }
191 
192     /**
193      * Update the media button session if needed.
194      * <p>The media button session is the session that will receive the media button events.
195      * <p>We send the media button events to the lastly played app. If the app has the media
196      * session, the session will receive the media button events.
197      */
updateMediaButtonSessionIfNeeded()198     public void updateMediaButtonSessionIfNeeded() {
199         if (DEBUG) {
200             Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + getCallers(2));
201         }
202         List<Integer> audioPlaybackUids =
203                 mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
204         for (int i = 0; i < audioPlaybackUids.size(); i++) {
205             int audioPlaybackUid = audioPlaybackUids.get(i);
206             MediaSessionRecordImpl mediaButtonSession = findMediaButtonSession(audioPlaybackUid);
207             if (mediaButtonSession == null) {
208                 if (DEBUG) {
209                     Log.d(TAG, "updateMediaButtonSessionIfNeeded, skipping uid="
210                             + audioPlaybackUid);
211                 }
212                 // Ignore if the lastly played app isn't a media app (i.e. has no media session)
213                 continue;
214             }
215             boolean ignoreButtonSession =
216                     (mediaButtonSession.getSessionPolicies()
217                             & SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0;
218             if (DEBUG) {
219                 Log.d(TAG, "updateMediaButtonSessionIfNeeded, checking uid=" + audioPlaybackUid
220                         + ", mediaButtonSession=" + mediaButtonSession
221                         + ", ignoreButtonSession=" + ignoreButtonSession);
222             }
223             if (!ignoreButtonSession) {
224                 mAudioPlayerStateMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
225                 if (mediaButtonSession != mMediaButtonSession) {
226                     updateMediaButtonSession(mediaButtonSession);
227                 }
228                 return;
229             }
230         }
231     }
232 
233     // TODO: Remove this and make updateMediaButtonSessionIfNeeded() to also cover this case.
updateMediaButtonSessionBySessionPolicyChange(MediaSessionRecord record)234     public void updateMediaButtonSessionBySessionPolicyChange(MediaSessionRecord record) {
235         if ((record.getSessionPolicies() & SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0) {
236             if (record == mMediaButtonSession) {
237                 // TODO(b/154456172): Make this decision unaltered by non-media app's playback.
238                 updateMediaButtonSession(null);
239             }
240         } else {
241             updateMediaButtonSessionIfNeeded();
242         }
243     }
244 
245     /**
246      * Find the media button session with the given {@param uid}.
247      * If the app has multiple media sessions, the media session whose playback state is not null
248      * and matches the audio playback state becomes the media button session. Otherwise the top
249      * priority session becomes the media button session.
250      *
251      * @return The media button session. Returns {@code null} if the app doesn't have a media
252      *   session.
253      */
findMediaButtonSession(int uid)254     private MediaSessionRecordImpl findMediaButtonSession(int uid) {
255         MediaSessionRecordImpl mediaButtonSession = null;
256         for (MediaSessionRecordImpl session : mSessions) {
257             if (session instanceof MediaSession2Record) {
258                 // TODO(jaewan): Make MediaSession2 to receive media key event
259                 continue;
260             }
261             if (uid == session.getUid()) {
262                 if (session.checkPlaybackActiveState(
263                         mAudioPlayerStateMonitor.isPlaybackActive(session.getUid()))) {
264                     // If there's a media session whose PlaybackState matches
265                     // the audio playback state, return it immediately.
266                     return session;
267                 }
268                 if (mediaButtonSession == null) {
269                     // Among the media sessions whose PlaybackState doesn't match
270                     // the audio playback state, pick the top priority.
271                     mediaButtonSession = session;
272                 }
273             }
274         }
275         return mediaButtonSession;
276     }
277 
278     /**
279      * Get the current priority sorted list of active sessions. The most
280      * important session is at index 0 and the least important at size - 1.
281      *
282      * @param userId The user to check. It can be {@link UserHandle#USER_ALL} to get all sessions
283      *    for all users in this {@link MediaSessionStack}.
284      * @return All the active sessions in priority order.
285      */
getActiveSessions(int userId)286     public List<MediaSessionRecord> getActiveSessions(int userId) {
287         List<MediaSessionRecord> cachedActiveList = mCachedActiveLists.get(userId);
288         if (cachedActiveList == null) {
289             cachedActiveList = getPriorityList(true, userId);
290             mCachedActiveLists.put(userId, cachedActiveList);
291         }
292         return cachedActiveList;
293     }
294 
295     /**
296      * Gets the session2 tokens.
297      *
298      * @param userId The user to check. It can be {@link UserHandle#USER_ALL} to get all session2
299      *    tokens for all users in this {@link MediaSessionStack}.
300      * @return All session2 tokens.
301      */
getSession2Tokens(int userId)302     public List<Session2Token> getSession2Tokens(int userId) {
303         ArrayList<Session2Token> session2Records = new ArrayList<>();
304         for (MediaSessionRecordImpl record : mSessions) {
305             if ((userId == UserHandle.USER_ALL || record.getUserId() == userId)
306                     && record.isActive()
307                     && record instanceof MediaSession2Record) {
308                 MediaSession2Record session2 = (MediaSession2Record) record;
309                 session2Records.add(session2.getSession2Token());
310             }
311         }
312         return session2Records;
313     }
314 
315     /**
316      * Get the media button session which receives the media button events.
317      *
318      * @return The media button session or null.
319      */
getMediaButtonSession()320     public MediaSessionRecordImpl getMediaButtonSession() {
321         return mMediaButtonSession;
322     }
323 
updateMediaButtonSession(MediaSessionRecordImpl newMediaButtonSession)324     public void updateMediaButtonSession(MediaSessionRecordImpl newMediaButtonSession) {
325         MediaSessionRecordImpl oldMediaButtonSession = mMediaButtonSession;
326         mMediaButtonSession = newMediaButtonSession;
327         mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(
328                 oldMediaButtonSession, newMediaButtonSession);
329     }
330 
getDefaultVolumeSession()331     public MediaSessionRecordImpl getDefaultVolumeSession() {
332         List<MediaSessionRecord> records = getPriorityList(true, UserHandle.USER_ALL);
333         int size = records.size();
334         for (int i = 0; i < size; i++) {
335             MediaSessionRecord record = records.get(i);
336             if (record.checkPlaybackActiveState(true) && record.canHandleVolumeKey()) {
337                 return record;
338             }
339         }
340         return null;
341     }
342 
getDefaultRemoteSession(int userId)343     public MediaSessionRecordImpl getDefaultRemoteSession(int userId) {
344         List<MediaSessionRecord> records = getPriorityList(true, userId);
345 
346         int size = records.size();
347         for (int i = 0; i < size; i++) {
348             MediaSessionRecord record = records.get(i);
349             if (!record.isPlaybackTypeLocal()) {
350                 return record;
351             }
352         }
353         return null;
354     }
355 
dump(PrintWriter pw, String prefix)356     public void dump(PrintWriter pw, String prefix) {
357         pw.println(prefix + "Media button session is " + mMediaButtonSession);
358         pw.println(prefix + "Sessions Stack - have " + mSessions.size() + " sessions:");
359         String indent = prefix + "  ";
360         for (MediaSessionRecordImpl record : mSessions) {
361             record.dump(pw, indent);
362         }
363     }
364 
365     /**
366      * Get a priority sorted list of sessions. Can filter to only return active
367      * sessions or sessions.
368      * <p>Here's the priority order.
369      * <li>Active sessions whose PlaybackState is active</li>
370      * <li>Active sessions whose PlaybackState is inactive</li>
371      * <li>Inactive sessions</li>
372      *
373      * @param activeOnly True to only return active sessions, false to return
374      *            all sessions.
375      * @param userId The user to get sessions for. {@link UserHandle#USER_ALL}
376      *            will return sessions for all users.
377      * @return The priority sorted list of sessions.
378      */
getPriorityList(boolean activeOnly, int userId)379     public List<MediaSessionRecord> getPriorityList(boolean activeOnly, int userId) {
380         List<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>();
381         int lastPlaybackActiveIndex = 0;
382         int lastActiveIndex = 0;
383 
384         for (MediaSessionRecordImpl record : mSessions) {
385             if (!(record instanceof MediaSessionRecord)) {
386                 continue;
387             }
388             final MediaSessionRecord session = (MediaSessionRecord) record;
389 
390             if ((userId != UserHandle.USER_ALL && userId != session.getUserId())) {
391                 // Filter out sessions for the wrong user or session2.
392                 continue;
393             }
394 
395             if (!session.isActive()) {
396                 if (!activeOnly) {
397                     // If we're getting unpublished as well always put them at
398                     // the end
399                     result.add(session);
400                 }
401                 continue;
402             }
403 
404             if (session.checkPlaybackActiveState(true)) {
405                 result.add(lastPlaybackActiveIndex++, session);
406                 lastActiveIndex++;
407             } else {
408                 result.add(lastActiveIndex++, session);
409             }
410         }
411 
412         return result;
413     }
414 
clearCache(int userId)415     private void clearCache(int userId) {
416         mCachedActiveLists.remove(userId);
417         // mCachedActiveLists may also include the list of sessions for UserHandle.USER_ALL,
418         // so they also need to be cleared.
419         mCachedActiveLists.remove(UserHandle.USER_ALL);
420     }
421 
422     // Code copied from android.os.Debug#getCallers(int)
getCallers(final int depth)423     private static String getCallers(final int depth) {
424         final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
425         StringBuilder sb = new StringBuilder();
426         for (int i = 0; i < depth; i++) {
427             sb.append(getCaller(callStack, i)).append(" ");
428         }
429         return sb.toString();
430     }
431 
432     // Code copied from android.os.Debug#getCaller(StackTraceElement[], int)
getCaller(StackTraceElement[] callStack, int depth)433     private static String getCaller(StackTraceElement[] callStack, int depth) {
434         // callStack[4] is the caller of the method that called getCallers()
435         if (4 + depth >= callStack.length) {
436             return "<bottom of call stack>";
437         }
438         StackTraceElement caller = callStack[4 + depth];
439         return caller.getClassName() + "." + caller.getMethodName() + ":" + caller.getLineNumber();
440     }
441 }
442