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