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