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.PlaybackState; 21 import android.media.session.MediaSession; 22 import android.os.UserHandle; 23 24 import java.io.PrintWriter; 25 import java.util.ArrayList; 26 27 /** 28 * Keeps track of media sessions and their priority for notifications, media 29 * button dispatch, etc. 30 */ 31 public class MediaSessionStack { 32 /** 33 * These are states that usually indicate the user took an action and should 34 * bump priority regardless of the old state. 35 */ 36 private static final int[] ALWAYS_PRIORITY_STATES = { 37 PlaybackState.STATE_FAST_FORWARDING, 38 PlaybackState.STATE_REWINDING, 39 PlaybackState.STATE_SKIPPING_TO_PREVIOUS, 40 PlaybackState.STATE_SKIPPING_TO_NEXT }; 41 /** 42 * These are states that usually indicate the user took an action if they 43 * were entered from a non-priority state. 44 */ 45 private static final int[] TRANSITION_PRIORITY_STATES = { 46 PlaybackState.STATE_BUFFERING, 47 PlaybackState.STATE_CONNECTING, 48 PlaybackState.STATE_PLAYING }; 49 50 private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>(); 51 52 private MediaSessionRecord mGlobalPrioritySession; 53 54 // The last record that either entered one of the playing states or was 55 // added. 56 private MediaSessionRecord mLastInterestingRecord; 57 private MediaSessionRecord mCachedButtonReceiver; 58 private MediaSessionRecord mCachedDefault; 59 private MediaSessionRecord mCachedVolumeDefault; 60 private ArrayList<MediaSessionRecord> mCachedActiveList; 61 private ArrayList<MediaSessionRecord> mCachedTransportControlList; 62 63 /** 64 * Add a record to the priority tracker. 65 * 66 * @param record The record to add. 67 */ 68 public void addSession(MediaSessionRecord record) { 69 mSessions.add(record); 70 clearCache(); 71 mLastInterestingRecord = record; 72 } 73 74 /** 75 * Remove a record from the priority tracker. 76 * 77 * @param record The record to remove. 78 */ 79 public void removeSession(MediaSessionRecord record) { 80 mSessions.remove(record); 81 if (record == mGlobalPrioritySession) { 82 mGlobalPrioritySession = null; 83 } 84 clearCache(); 85 } 86 87 /** 88 * Notify the priority tracker that a session's state changed. 89 * 90 * @param record The record that changed. 91 * @param oldState Its old playback state. 92 * @param newState Its new playback state. 93 * @return true if the priority order was updated, false otherwise. 94 */ 95 public boolean onPlaystateChange(MediaSessionRecord record, int oldState, int newState) { 96 if (shouldUpdatePriority(oldState, newState)) { 97 mSessions.remove(record); 98 mSessions.add(0, record); 99 clearCache(); 100 // This becomes the last interesting record since it entered a 101 // playing state 102 mLastInterestingRecord = record; 103 return true; 104 } else if (!MediaSession.isActiveState(newState)) { 105 // Just clear the volume cache when a state goes inactive 106 mCachedVolumeDefault = null; 107 } 108 return false; 109 } 110 111 /** 112 * Handle any stack changes that need to occur in response to a session 113 * state change. TODO add the old and new session state as params 114 * 115 * @param record The record that changed. 116 */ 117 public void onSessionStateChange(MediaSessionRecord record) { 118 if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) { 119 mGlobalPrioritySession = record; 120 } 121 // For now just clear the cache. Eventually we'll selectively clear 122 // depending on what changed. 123 clearCache(); 124 } 125 126 /** 127 * Get the current priority sorted list of active sessions. The most 128 * important session is at index 0 and the least important at size - 1. 129 * 130 * @param userId The user to check. 131 * @return All the active sessions in priority order. 132 */ 133 public ArrayList<MediaSessionRecord> getActiveSessions(int userId) { 134 if (mCachedActiveList == null) { 135 mCachedActiveList = getPriorityListLocked(true, 0, userId); 136 } 137 return mCachedActiveList; 138 } 139 140 /** 141 * Get the current priority sorted list of active sessions that use 142 * transport controls. The most important session is at index 0 and the 143 * least important at size -1. 144 * 145 * @param userId The user to check. 146 * @return All the active sessions that handle transport controls in 147 * priority order. 148 */ 149 public ArrayList<MediaSessionRecord> getTransportControlSessions(int userId) { 150 if (mCachedTransportControlList == null) { 151 mCachedTransportControlList = getPriorityListLocked(true, 152 MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS, userId); 153 } 154 return mCachedTransportControlList; 155 } 156 157 /** 158 * Get the highest priority active session. 159 * 160 * @param userId The user to check. 161 * @return The current highest priority session or null. 162 */ 163 public MediaSessionRecord getDefaultSession(int userId) { 164 if (mCachedDefault != null) { 165 return mCachedDefault; 166 } 167 ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId); 168 if (records.size() > 0) { 169 return records.get(0); 170 } 171 return null; 172 } 173 174 /** 175 * Get the highest priority session that can handle media buttons. 176 * 177 * @param userId The user to check. 178 * @param includeNotPlaying Return a non-playing session if nothing else is 179 * available 180 * @return The default media button session or null. 181 */ 182 public MediaSessionRecord getDefaultMediaButtonSession(int userId, boolean includeNotPlaying) { 183 if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) { 184 return mGlobalPrioritySession; 185 } 186 if (mCachedButtonReceiver != null) { 187 return mCachedButtonReceiver; 188 } 189 ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 190 MediaSession.FLAG_HANDLES_MEDIA_BUTTONS, userId); 191 if (records.size() > 0) { 192 MediaSessionRecord record = records.get(0); 193 if (record.isPlaybackActive(false)) { 194 // Since we're going to send a button event to this record make 195 // it the last interesting one. 196 mLastInterestingRecord = record; 197 mCachedButtonReceiver = record; 198 } else if (mLastInterestingRecord != null) { 199 if (records.contains(mLastInterestingRecord)) { 200 mCachedButtonReceiver = mLastInterestingRecord; 201 } else { 202 // That record is no longer used. Clear its reference. 203 mLastInterestingRecord = null; 204 } 205 } 206 if (includeNotPlaying && mCachedButtonReceiver == null) { 207 // If we really want a record and we didn't find one yet use the 208 // highest priority session even if it's not playing. 209 mCachedButtonReceiver = record; 210 } 211 } 212 return mCachedButtonReceiver; 213 } 214 215 public MediaSessionRecord getDefaultVolumeSession(int userId) { 216 if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) { 217 return mGlobalPrioritySession; 218 } 219 if (mCachedVolumeDefault != null) { 220 return mCachedVolumeDefault; 221 } 222 ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId); 223 int size = records.size(); 224 for (int i = 0; i < size; i++) { 225 MediaSessionRecord record = records.get(i); 226 if (record.isPlaybackActive(false)) { 227 mCachedVolumeDefault = record; 228 return record; 229 } 230 } 231 return null; 232 } 233 234 public MediaSessionRecord getDefaultRemoteSession(int userId) { 235 ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId); 236 237 int size = records.size(); 238 for (int i = 0; i < size; i++) { 239 MediaSessionRecord record = records.get(i); 240 if (record.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE) { 241 return record; 242 } 243 } 244 return null; 245 } 246 247 public boolean isGlobalPriorityActive() { 248 return mGlobalPrioritySession == null ? false : mGlobalPrioritySession.isActive(); 249 } 250 251 public void dump(PrintWriter pw, String prefix) { 252 ArrayList<MediaSessionRecord> sortedSessions = getPriorityListLocked(false, 0, 253 UserHandle.USER_ALL); 254 int count = sortedSessions.size(); 255 pw.println(prefix + "Global priority session is " + mGlobalPrioritySession); 256 pw.println(prefix + "Sessions Stack - have " + count + " sessions:"); 257 String indent = prefix + " "; 258 for (int i = 0; i < count; i++) { 259 MediaSessionRecord record = sortedSessions.get(i); 260 record.dump(pw, indent); 261 pw.println(); 262 } 263 } 264 265 /** 266 * Get a priority sorted list of sessions. Can filter to only return active 267 * sessions or sessions with specific flags. 268 * 269 * @param activeOnly True to only return active sessions, false to return 270 * all sessions. 271 * @param withFlags Only return sessions with all the specified flags set. 0 272 * returns all sessions. 273 * @param userId The user to get sessions for. {@link UserHandle#USER_ALL} 274 * will return sessions for all users. 275 * @return The priority sorted list of sessions. 276 */ 277 private ArrayList<MediaSessionRecord> getPriorityListLocked(boolean activeOnly, int withFlags, 278 int userId) { 279 ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>(); 280 int lastLocalIndex = 0; 281 int lastActiveIndex = 0; 282 int lastPublishedIndex = 0; 283 284 int size = mSessions.size(); 285 for (int i = 0; i < size; i++) { 286 final MediaSessionRecord session = mSessions.get(i); 287 288 if (userId != UserHandle.USER_ALL && userId != session.getUserId()) { 289 // Filter out sessions for the wrong user 290 continue; 291 } 292 if ((session.getFlags() & withFlags) != withFlags) { 293 // Filter out sessions with the wrong flags 294 continue; 295 } 296 if (!session.isActive()) { 297 if (!activeOnly) { 298 // If we're getting unpublished as well always put them at 299 // the end 300 result.add(session); 301 } 302 continue; 303 } 304 305 if (session.isSystemPriority()) { 306 // System priority sessions are special and always go at the 307 // front. We expect there to only be one of these at a time. 308 result.add(0, session); 309 lastLocalIndex++; 310 lastActiveIndex++; 311 lastPublishedIndex++; 312 } else if (session.isPlaybackActive(true)) { 313 // TODO this with real local route check 314 if (true) { 315 // Active local sessions get top priority 316 result.add(lastLocalIndex, session); 317 lastLocalIndex++; 318 lastActiveIndex++; 319 lastPublishedIndex++; 320 } else { 321 // Then active remote sessions 322 result.add(lastActiveIndex, session); 323 lastActiveIndex++; 324 lastPublishedIndex++; 325 } 326 } else { 327 // inactive sessions go at the end in order of whoever last did 328 // something. 329 result.add(lastPublishedIndex, session); 330 lastPublishedIndex++; 331 } 332 } 333 334 return result; 335 } 336 337 private boolean shouldUpdatePriority(int oldState, int newState) { 338 if (containsState(newState, ALWAYS_PRIORITY_STATES)) { 339 return true; 340 } 341 if (!containsState(oldState, TRANSITION_PRIORITY_STATES) 342 && containsState(newState, TRANSITION_PRIORITY_STATES)) { 343 return true; 344 } 345 return false; 346 } 347 348 private boolean containsState(int state, int[] states) { 349 for (int i = 0; i < states.length; i++) { 350 if (states[i] == state) { 351 return true; 352 } 353 } 354 return false; 355 } 356 357 private void clearCache() { 358 mCachedDefault = null; 359 mCachedVolumeDefault = null; 360 mCachedButtonReceiver = null; 361 mCachedActiveList = null; 362 mCachedTransportControlList = null; 363 } 364 } 365