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