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