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 android.media.session.MediaController.PlaybackInfo; 20 import android.media.session.MediaSession; 21 import android.media.session.PlaybackState; 22 import android.os.Debug; 23 import android.os.UserHandle; 24 import android.util.IntArray; 25 import android.util.Log; 26 import android.util.SparseArray; 27 28 import java.io.PrintWriter; 29 import java.util.ArrayList; 30 import java.util.List; 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 */ 48 void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession, 49 MediaSessionRecord newMediaButtonSession); 50 } 51 52 /** 53 * These are states that usually indicate the user took an action and should 54 * bump priority regardless of the old state. 55 */ 56 private static final int[] ALWAYS_PRIORITY_STATES = { 57 PlaybackState.STATE_FAST_FORWARDING, 58 PlaybackState.STATE_REWINDING, 59 PlaybackState.STATE_SKIPPING_TO_PREVIOUS, 60 PlaybackState.STATE_SKIPPING_TO_NEXT }; 61 /** 62 * These are states that usually indicate the user took an action if they 63 * were entered from a non-priority state. 64 */ 65 private static final int[] TRANSITION_PRIORITY_STATES = { 66 PlaybackState.STATE_BUFFERING, 67 PlaybackState.STATE_CONNECTING, 68 PlaybackState.STATE_PLAYING }; 69 70 /** 71 * Sorted list of the media sessions. 72 * The session of which PlaybackState is changed to ALWAYS_PRIORITY_STATES or 73 * TRANSITION_PRIORITY_STATES comes first. 74 * @see #shouldUpdatePriority 75 */ 76 private final List<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>(); 77 78 private final AudioPlayerStateMonitor mAudioPlayerStateMonitor; 79 private final OnMediaButtonSessionChangedListener mOnMediaButtonSessionChangedListener; 80 81 /** 82 * The media button session which receives media key events. 83 * It could be null if the previous media buttion session is released. 84 */ 85 private MediaSessionRecord mMediaButtonSession; 86 87 private MediaSessionRecord mCachedVolumeDefault; 88 89 /** 90 * Cache the result of the {@link #getActiveSessions} per user. 91 */ 92 private final SparseArray<ArrayList<MediaSessionRecord>> mCachedActiveLists = 93 new SparseArray<>(); 94 95 MediaSessionStack(AudioPlayerStateMonitor monitor, OnMediaButtonSessionChangedListener listener) { 96 mAudioPlayerStateMonitor = monitor; 97 mOnMediaButtonSessionChangedListener = listener; 98 } 99 100 /** 101 * Add a record to the priority tracker. 102 * 103 * @param record The record to add. 104 */ 105 public void addSession(MediaSessionRecord record) { 106 mSessions.add(record); 107 clearCache(record.getUserId()); 108 109 // Update the media button session. 110 // The added session could be the session from the package with the audio playback. 111 // This can happen if an app starts audio playback before creating media session. 112 updateMediaButtonSessionIfNeeded(); 113 } 114 115 /** 116 * Remove a record from the priority tracker. 117 * 118 * @param record The record to remove. 119 */ 120 public void removeSession(MediaSessionRecord record) { 121 mSessions.remove(record); 122 if (mMediaButtonSession == record) { 123 // When the media button session is removed, nullify the media button session and do not 124 // search for the alternative media session within the app. It's because the alternative 125 // media session might be a dummy which isn't able to handle the media key events. 126 updateMediaButtonSession(null); 127 } 128 clearCache(record.getUserId()); 129 } 130 131 /** 132 * Return if the record exists in the priority tracker. 133 */ 134 public boolean contains(MediaSessionRecord record) { 135 return mSessions.contains(record); 136 } 137 138 /** 139 * Notify the priority tracker that a session's playback state changed. 140 * 141 * @param record The record that changed. 142 * @param oldState Its old playback state. 143 * @param newState Its new playback state. 144 */ 145 public void onPlaystateChanged(MediaSessionRecord record, int oldState, int newState) { 146 if (shouldUpdatePriority(oldState, newState)) { 147 mSessions.remove(record); 148 mSessions.add(0, record); 149 clearCache(record.getUserId()); 150 } else if (!MediaSession.isActiveState(newState)) { 151 // Just clear the volume cache when a state goes inactive 152 mCachedVolumeDefault = null; 153 } 154 155 // In most cases, playback state isn't needed for finding media button session, 156 // but we only use it as a hint if an app has multiple local media sessions. 157 // In that case, we pick the media session whose PlaybackState matches 158 // the audio playback configuration. 159 if (mMediaButtonSession != null && mMediaButtonSession.getUid() == record.getUid()) { 160 MediaSessionRecord newMediaButtonSession = 161 findMediaButtonSession(mMediaButtonSession.getUid()); 162 if (newMediaButtonSession != mMediaButtonSession) { 163 updateMediaButtonSession(newMediaButtonSession); 164 } 165 } 166 } 167 168 /** 169 * Handle the change in activeness for a session. 170 * 171 * @param record The record that changed. 172 */ 173 public void onSessionStateChange(MediaSessionRecord record) { 174 // For now just clear the cache. Eventually we'll selectively clear 175 // depending on what changed. 176 clearCache(record.getUserId()); 177 } 178 179 /** 180 * Update the media button session if needed. 181 * <p>The media button session is the session that will receive the media button events. 182 * <p>We send the media button events to the lastly played app. If the app has the media 183 * session, the session will receive the media button events. 184 */ 185 public void updateMediaButtonSessionIfNeeded() { 186 if (DEBUG) { 187 Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + Debug.getCallers(2)); 188 } 189 IntArray audioPlaybackUids = mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids(); 190 for (int i = 0; i < audioPlaybackUids.size(); i++) { 191 MediaSessionRecord mediaButtonSession = 192 findMediaButtonSession(audioPlaybackUids.get(i)); 193 if (mediaButtonSession != null) { 194 // Found the media button session. 195 mAudioPlayerStateMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid()); 196 if (mMediaButtonSession != mediaButtonSession) { 197 updateMediaButtonSession(mediaButtonSession); 198 } 199 return; 200 } 201 } 202 } 203 204 /** 205 * Find the media button session with the given {@param uid}. 206 * If the app has multiple media sessions, the media session whose playback state is not null 207 * and matches the audio playback state becomes the media button session. Otherwise the top 208 * priority session becomes the media button session. 209 * 210 * @return The media button session. Returns {@code null} if the app doesn't have a media 211 * session. 212 */ 213 private MediaSessionRecord findMediaButtonSession(int uid) { 214 MediaSessionRecord mediaButtonSession = null; 215 for (MediaSessionRecord session : mSessions) { 216 if (uid == session.getUid()) { 217 if (session.getPlaybackState() != null && session.isPlaybackActive() == 218 mAudioPlayerStateMonitor.isPlaybackActive(session.getUid())) { 219 // If there's a media session whose PlaybackState matches 220 // the audio playback state, return it immediately. 221 return session; 222 } 223 if (mediaButtonSession == null) { 224 // Among the media sessions whose PlaybackState doesn't match 225 // the audio playback state, pick the top priority. 226 mediaButtonSession = session; 227 } 228 } 229 } 230 return mediaButtonSession; 231 } 232 233 /** 234 * Get the current priority sorted list of active sessions. The most 235 * important session is at index 0 and the least important at size - 1. 236 * 237 * @param userId The user to check. It can be {@link UserHandle#USER_ALL} to get all sessions 238 * for all users in this {@link MediaSessionStack}. 239 * @return All the active sessions in priority order. 240 */ 241 public ArrayList<MediaSessionRecord> getActiveSessions(int userId) { 242 ArrayList<MediaSessionRecord> cachedActiveList = mCachedActiveLists.get(userId); 243 if (cachedActiveList == null) { 244 cachedActiveList = getPriorityList(true, userId); 245 mCachedActiveLists.put(userId, cachedActiveList); 246 } 247 return cachedActiveList; 248 } 249 250 /** 251 * Get the media button session which receives the media button events. 252 * 253 * @return The media button session or null. 254 */ 255 public MediaSessionRecord getMediaButtonSession() { 256 return mMediaButtonSession; 257 } 258 259 private void updateMediaButtonSession(MediaSessionRecord newMediaButtonSession) { 260 MediaSessionRecord oldMediaButtonSession = mMediaButtonSession; 261 mMediaButtonSession = newMediaButtonSession; 262 mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged( 263 oldMediaButtonSession, newMediaButtonSession); 264 } 265 266 public MediaSessionRecord getDefaultVolumeSession() { 267 if (mCachedVolumeDefault != null) { 268 return mCachedVolumeDefault; 269 } 270 ArrayList<MediaSessionRecord> records = getPriorityList(true, UserHandle.USER_ALL); 271 int size = records.size(); 272 for (int i = 0; i < size; i++) { 273 MediaSessionRecord record = records.get(i); 274 if (record.isPlaybackActive()) { 275 mCachedVolumeDefault = record; 276 return record; 277 } 278 } 279 return null; 280 } 281 282 public MediaSessionRecord getDefaultRemoteSession(int userId) { 283 ArrayList<MediaSessionRecord> records = getPriorityList(true, userId); 284 285 int size = records.size(); 286 for (int i = 0; i < size; i++) { 287 MediaSessionRecord record = records.get(i); 288 if (record.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE) { 289 return record; 290 } 291 } 292 return null; 293 } 294 295 public void dump(PrintWriter pw, String prefix) { 296 ArrayList<MediaSessionRecord> sortedSessions = getPriorityList(false, 297 UserHandle.USER_ALL); 298 int count = sortedSessions.size(); 299 pw.println(prefix + "Media button session is " + mMediaButtonSession); 300 pw.println(prefix + "Sessions Stack - have " + count + " sessions:"); 301 String indent = prefix + " "; 302 for (int i = 0; i < count; i++) { 303 MediaSessionRecord record = sortedSessions.get(i); 304 record.dump(pw, indent); 305 pw.println(); 306 } 307 } 308 309 /** 310 * Get a priority sorted list of sessions. Can filter to only return active 311 * sessions or sessions. 312 * <p>Here's the priority order. 313 * <li>Active sessions whose PlaybackState is active</li> 314 * <li>Active sessions whose PlaybackState is inactive</li> 315 * <li>Inactive sessions</li> 316 * 317 * @param activeOnly True to only return active sessions, false to return 318 * all sessions. 319 * @param userId The user to get sessions for. {@link UserHandle#USER_ALL} 320 * will return sessions for all users. 321 * @return The priority sorted list of sessions. 322 */ 323 public ArrayList<MediaSessionRecord> getPriorityList(boolean activeOnly, int userId) { 324 ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>(); 325 int lastPlaybackActiveIndex = 0; 326 int lastActiveIndex = 0; 327 328 int size = mSessions.size(); 329 for (int i = 0; i < size; i++) { 330 final MediaSessionRecord session = mSessions.get(i); 331 332 if (userId != UserHandle.USER_ALL && userId != session.getUserId()) { 333 // Filter out sessions for the wrong user 334 continue; 335 } 336 337 if (!session.isActive()) { 338 if (!activeOnly) { 339 // If we're getting unpublished as well always put them at 340 // the end 341 result.add(session); 342 } 343 continue; 344 } 345 346 if (session.isPlaybackActive()) { 347 result.add(lastPlaybackActiveIndex++, session); 348 lastActiveIndex++; 349 } else { 350 result.add(lastActiveIndex++, session); 351 } 352 } 353 354 return result; 355 } 356 357 private boolean shouldUpdatePriority(int oldState, int newState) { 358 if (containsState(newState, ALWAYS_PRIORITY_STATES)) { 359 return true; 360 } 361 if (!containsState(oldState, TRANSITION_PRIORITY_STATES) 362 && containsState(newState, TRANSITION_PRIORITY_STATES)) { 363 return true; 364 } 365 return false; 366 } 367 368 private boolean containsState(int state, int[] states) { 369 for (int i = 0; i < states.length; i++) { 370 if (states[i] == state) { 371 return true; 372 } 373 } 374 return false; 375 } 376 377 private void clearCache(int userId) { 378 mCachedVolumeDefault = null; 379 mCachedActiveLists.remove(userId); 380 // mCachedActiveLists may also include the list of sessions for UserHandle.USER_ALL, 381 // so they also need to be cleared. 382 mCachedActiveLists.remove(UserHandle.USER_ALL); 383 } 384 } 385