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