• 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     private MediaSessionRecordImpl mCachedVolumeDefault;
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         } else if (record.checkPlaybackActiveState(false)) {
163             // Just clear the volume cache when a state goes inactive
164             mCachedVolumeDefault = null;
165         }
166 
167         // In most cases, playback state isn't needed for finding media button session,
168         // but we only use it as a hint if an app has multiple local media sessions.
169         // In that case, we pick the media session whose PlaybackState matches
170         // the audio playback configuration.
171         if (mMediaButtonSession != null && mMediaButtonSession.getUid() == record.getUid()) {
172             MediaSessionRecordImpl newMediaButtonSession =
173                     findMediaButtonSession(mMediaButtonSession.getUid());
174             if (newMediaButtonSession != mMediaButtonSession
175                     && (newMediaButtonSession.getSessionPolicies()
176                             & SESSION_POLICY_IGNORE_BUTTON_SESSION) == 0) {
177                 // Check if the policy states that this session should not be updated as a media
178                 // button session.
179                 updateMediaButtonSession(newMediaButtonSession);
180             }
181         }
182     }
183 
184     /**
185      * Handle the change in activeness for a session.
186      *
187      * @param record The record that changed.
188      */
onSessionActiveStateChanged(MediaSessionRecordImpl record)189     public void onSessionActiveStateChanged(MediaSessionRecordImpl record) {
190         // For now just clear the cache. Eventually we'll selectively clear
191         // depending on what changed.
192         clearCache(record.getUserId());
193     }
194 
195     /**
196      * Update the media button session if needed.
197      * <p>The media button session is the session that will receive the media button events.
198      * <p>We send the media button events to the lastly played app. If the app has the media
199      * session, the session will receive the media button events.
200      */
updateMediaButtonSessionIfNeeded()201     public void updateMediaButtonSessionIfNeeded() {
202         if (DEBUG) {
203             Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + getCallers(2));
204         }
205         List<Integer> audioPlaybackUids =
206                 mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
207         for (int i = 0; i < audioPlaybackUids.size(); i++) {
208             int audioPlaybackUid = audioPlaybackUids.get(i);
209             MediaSessionRecordImpl mediaButtonSession = findMediaButtonSession(audioPlaybackUid);
210             if (mediaButtonSession == null) {
211                 if (DEBUG) {
212                     Log.d(TAG, "updateMediaButtonSessionIfNeeded, skipping uid="
213                             + audioPlaybackUid);
214                 }
215                 // Ignore if the lastly played app isn't a media app (i.e. has no media session)
216                 continue;
217             }
218             boolean ignoreButtonSession =
219                     (mediaButtonSession.getSessionPolicies()
220                             & SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0;
221             if (DEBUG) {
222                 Log.d(TAG, "updateMediaButtonSessionIfNeeded, checking uid=" + audioPlaybackUid
223                         + ", mediaButtonSession=" + mediaButtonSession
224                         + ", ignoreButtonSession=" + ignoreButtonSession);
225             }
226             if (!ignoreButtonSession) {
227                 mAudioPlayerStateMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
228                 if (mediaButtonSession != mMediaButtonSession) {
229                     updateMediaButtonSession(mediaButtonSession);
230                 }
231                 return;
232             }
233         }
234     }
235 
236     // TODO: Remove this and make updateMediaButtonSessionIfNeeded() to also cover this case.
updateMediaButtonSessionBySessionPolicyChange(MediaSessionRecord record)237     public void updateMediaButtonSessionBySessionPolicyChange(MediaSessionRecord record) {
238         if ((record.getSessionPolicies() & SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0) {
239             if (record == mMediaButtonSession) {
240                 // TODO(b/154456172): Make this decision unaltered by non-media app's playback.
241                 updateMediaButtonSession(null);
242             }
243         } else {
244             updateMediaButtonSessionIfNeeded();
245         }
246     }
247 
248     /**
249      * Find the media button session with the given {@param uid}.
250      * If the app has multiple media sessions, the media session whose playback state is not null
251      * and matches the audio playback state becomes the media button session. Otherwise the top
252      * priority session becomes the media button session.
253      *
254      * @return The media button session. Returns {@code null} if the app doesn't have a media
255      *   session.
256      */
findMediaButtonSession(int uid)257     private MediaSessionRecordImpl findMediaButtonSession(int uid) {
258         MediaSessionRecordImpl mediaButtonSession = null;
259         for (MediaSessionRecordImpl session : mSessions) {
260             if (session instanceof MediaSession2Record) {
261                 // TODO(jaewan): Make MediaSession2 to receive media key event
262                 continue;
263             }
264             if (uid == session.getUid()) {
265                 if (session.checkPlaybackActiveState(
266                         mAudioPlayerStateMonitor.isPlaybackActive(session.getUid()))) {
267                     // If there's a media session whose PlaybackState matches
268                     // the audio playback state, return it immediately.
269                     return session;
270                 }
271                 if (mediaButtonSession == null) {
272                     // Among the media sessions whose PlaybackState doesn't match
273                     // the audio playback state, pick the top priority.
274                     mediaButtonSession = session;
275                 }
276             }
277         }
278         return mediaButtonSession;
279     }
280 
281     /**
282      * Get the current priority sorted list of active sessions. The most
283      * important session is at index 0 and the least important at size - 1.
284      *
285      * @param userId The user to check. It can be {@link UserHandle#USER_ALL} to get all sessions
286      *    for all users in this {@link MediaSessionStack}.
287      * @return All the active sessions in priority order.
288      */
getActiveSessions(int userId)289     public List<MediaSessionRecord> getActiveSessions(int userId) {
290         List<MediaSessionRecord> cachedActiveList = mCachedActiveLists.get(userId);
291         if (cachedActiveList == null) {
292             cachedActiveList = getPriorityList(true, userId);
293             mCachedActiveLists.put(userId, cachedActiveList);
294         }
295         return cachedActiveList;
296     }
297 
298     /**
299      * Gets the session2 tokens.
300      *
301      * @param userId The user to check. It can be {@link UserHandle#USER_ALL} to get all session2
302      *    tokens for all users in this {@link MediaSessionStack}.
303      * @return All session2 tokens.
304      */
getSession2Tokens(int userId)305     public List<Session2Token> getSession2Tokens(int userId) {
306         ArrayList<Session2Token> session2Records = new ArrayList<>();
307         for (MediaSessionRecordImpl record : mSessions) {
308             if ((userId == UserHandle.USER_ALL || record.getUserId() == userId)
309                     && record.isActive()
310                     && record instanceof MediaSession2Record) {
311                 MediaSession2Record session2 = (MediaSession2Record) record;
312                 session2Records.add(session2.getSession2Token());
313             }
314         }
315         return session2Records;
316     }
317 
318     /**
319      * Get the media button session which receives the media button events.
320      *
321      * @return The media button session or null.
322      */
getMediaButtonSession()323     public MediaSessionRecordImpl getMediaButtonSession() {
324         return mMediaButtonSession;
325     }
326 
updateMediaButtonSession(MediaSessionRecordImpl newMediaButtonSession)327     public void updateMediaButtonSession(MediaSessionRecordImpl newMediaButtonSession) {
328         MediaSessionRecordImpl oldMediaButtonSession = mMediaButtonSession;
329         mMediaButtonSession = newMediaButtonSession;
330         mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(
331                 oldMediaButtonSession, newMediaButtonSession);
332     }
333 
getDefaultVolumeSession()334     public MediaSessionRecordImpl getDefaultVolumeSession() {
335         if (mCachedVolumeDefault != null) {
336             return mCachedVolumeDefault;
337         }
338         List<MediaSessionRecord> records = getPriorityList(true, UserHandle.USER_ALL);
339         int size = records.size();
340         for (int i = 0; i < size; i++) {
341             MediaSessionRecord record = records.get(i);
342             if (record.checkPlaybackActiveState(true) && record.canHandleVolumeKey()) {
343                 mCachedVolumeDefault = record;
344                 return record;
345             }
346         }
347         return null;
348     }
349 
getDefaultRemoteSession(int userId)350     public MediaSessionRecordImpl getDefaultRemoteSession(int userId) {
351         List<MediaSessionRecord> records = getPriorityList(true, userId);
352 
353         int size = records.size();
354         for (int i = 0; i < size; i++) {
355             MediaSessionRecord record = records.get(i);
356             if (!record.isPlaybackTypeLocal()) {
357                 return record;
358             }
359         }
360         return null;
361     }
362 
dump(PrintWriter pw, String prefix)363     public void dump(PrintWriter pw, String prefix) {
364         pw.println(prefix + "Media button session is " + mMediaButtonSession);
365         pw.println(prefix + "Sessions Stack - have " + mSessions.size() + " sessions:");
366         String indent = prefix + "  ";
367         for (MediaSessionRecordImpl record : mSessions) {
368             record.dump(pw, indent);
369         }
370     }
371 
372     /**
373      * Get a priority sorted list of sessions. Can filter to only return active
374      * sessions or sessions.
375      * <p>Here's the priority order.
376      * <li>Active sessions whose PlaybackState is active</li>
377      * <li>Active sessions whose PlaybackState is inactive</li>
378      * <li>Inactive sessions</li>
379      *
380      * @param activeOnly True to only return active sessions, false to return
381      *            all sessions.
382      * @param userId The user to get sessions for. {@link UserHandle#USER_ALL}
383      *            will return sessions for all users.
384      * @return The priority sorted list of sessions.
385      */
getPriorityList(boolean activeOnly, int userId)386     public List<MediaSessionRecord> getPriorityList(boolean activeOnly, int userId) {
387         List<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>();
388         int lastPlaybackActiveIndex = 0;
389         int lastActiveIndex = 0;
390 
391         for (MediaSessionRecordImpl record : mSessions) {
392             if (!(record instanceof MediaSessionRecord)) {
393                 continue;
394             }
395             final MediaSessionRecord session = (MediaSessionRecord) record;
396 
397             if ((userId != UserHandle.USER_ALL && userId != session.getUserId())) {
398                 // Filter out sessions for the wrong user or session2.
399                 continue;
400             }
401 
402             if (!session.isActive()) {
403                 if (!activeOnly) {
404                     // If we're getting unpublished as well always put them at
405                     // the end
406                     result.add(session);
407                 }
408                 continue;
409             }
410 
411             if (session.checkPlaybackActiveState(true)) {
412                 result.add(lastPlaybackActiveIndex++, session);
413                 lastActiveIndex++;
414             } else {
415                 result.add(lastActiveIndex++, session);
416             }
417         }
418 
419         return result;
420     }
421 
clearCache(int userId)422     private void clearCache(int userId) {
423         mCachedVolumeDefault = null;
424         mCachedActiveLists.remove(userId);
425         // mCachedActiveLists may also include the list of sessions for UserHandle.USER_ALL,
426         // so they also need to be cleared.
427         mCachedActiveLists.remove(UserHandle.USER_ALL);
428     }
429 
430     // Code copied from android.os.Debug#getCallers(int)
getCallers(final int depth)431     private static String getCallers(final int depth) {
432         final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
433         StringBuilder sb = new StringBuilder();
434         for (int i = 0; i < depth; i++) {
435             sb.append(getCaller(callStack, i)).append(" ");
436         }
437         return sb.toString();
438     }
439 
440     // Code copied from android.os.Debug#getCaller(StackTraceElement[], int)
getCaller(StackTraceElement[] callStack, int depth)441     private static String getCaller(StackTraceElement[] callStack, int depth) {
442         // callStack[4] is the caller of the method that called getCallers()
443         if (4 + depth >= callStack.length) {
444             return "<bottom of call stack>";
445         }
446         StackTraceElement caller = callStack[4 + depth];
447         return caller.getClassName() + "." + caller.getMethodName() + ":" + caller.getLineNumber();
448     }
449 }
450