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