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