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