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.app.ActivityManager; 20 import android.app.ActivityManagerNative; 21 import android.media.session.MediaController.PlaybackInfo; 22 import android.media.session.PlaybackState; 23 import android.media.session.MediaSession; 24 import android.os.RemoteException; 25 import android.os.UserHandle; 26 27 import java.io.PrintWriter; 28 import java.util.ArrayList; 29 import java.util.List; 30 31 /** 32 * Keeps track of media sessions and their priority for notifications, media 33 * button dispatch, etc. 34 */ 35 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 {@code true} if the media session's package name equals to the most recent app, false 72 * otherwise. 73 */ isFromMostRecentApp(MediaSessionRecord record)74 private static boolean isFromMostRecentApp(MediaSessionRecord record) { 75 try { 76 List<ActivityManager.RecentTaskInfo> tasks = 77 ActivityManagerNative.getDefault().getRecentTasks(1, 78 ActivityManager.RECENT_IGNORE_HOME_STACK_TASKS | 79 ActivityManager.RECENT_IGNORE_UNAVAILABLE | 80 ActivityManager.RECENT_INCLUDE_PROFILES | 81 ActivityManager.RECENT_WITH_EXCLUDED, record.getUserId()).getList(); 82 if (tasks != null && !tasks.isEmpty()) { 83 ActivityManager.RecentTaskInfo recentTask = tasks.get(0); 84 if (recentTask.userId == record.getUserId() && recentTask.baseIntent != null) { 85 return recentTask.baseIntent.getComponent().getPackageName() 86 .equals(record.getPackageName()); 87 } 88 } 89 } catch (RemoteException e) { 90 return false; 91 } 92 return false; 93 } 94 95 /** 96 * Add a record to the priority tracker. 97 * 98 * @param record The record to add. 99 * @param fromForegroundUser {@code true} if the session is created by the foreground user. 100 */ addSession(MediaSessionRecord record, boolean fromForegroundUser)101 public void addSession(MediaSessionRecord record, boolean fromForegroundUser) { 102 mSessions.add(record); 103 clearCache(); 104 if (fromForegroundUser && isFromMostRecentApp(record)) { 105 mLastInterestingRecord = record; 106 } 107 } 108 109 /** 110 * Remove a record from the priority tracker. 111 * 112 * @param record The record to remove. 113 */ removeSession(MediaSessionRecord record)114 public void removeSession(MediaSessionRecord record) { 115 mSessions.remove(record); 116 if (record == mGlobalPrioritySession) { 117 mGlobalPrioritySession = null; 118 } 119 clearCache(); 120 } 121 122 /** 123 * Notify the priority tracker that a session's state changed. 124 * 125 * @param record The record that changed. 126 * @param oldState Its old playback state. 127 * @param newState Its new playback state. 128 * @return true if the priority order was updated, false otherwise. 129 */ onPlaystateChange(MediaSessionRecord record, int oldState, int newState)130 public boolean onPlaystateChange(MediaSessionRecord record, int oldState, int newState) { 131 if (shouldUpdatePriority(oldState, newState)) { 132 mSessions.remove(record); 133 mSessions.add(0, record); 134 clearCache(); 135 // This becomes the last interesting record since it entered a 136 // playing state 137 mLastInterestingRecord = record; 138 return true; 139 } else if (!MediaSession.isActiveState(newState)) { 140 // Just clear the volume cache when a state goes inactive 141 mCachedVolumeDefault = null; 142 } 143 return false; 144 } 145 146 /** 147 * Handle any stack changes that need to occur in response to a session 148 * state change. TODO add the old and new session state as params 149 * 150 * @param record The record that changed. 151 */ onSessionStateChange(MediaSessionRecord record)152 public void onSessionStateChange(MediaSessionRecord record) { 153 if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) { 154 mGlobalPrioritySession = record; 155 } 156 // For now just clear the cache. Eventually we'll selectively clear 157 // depending on what changed. 158 clearCache(); 159 } 160 161 /** 162 * Get the current priority sorted list of active sessions. The most 163 * important session is at index 0 and the least important at size - 1. 164 * 165 * @param userId The user to check. 166 * @return All the active sessions in priority order. 167 */ getActiveSessions(int userId)168 public ArrayList<MediaSessionRecord> getActiveSessions(int userId) { 169 if (mCachedActiveList == null) { 170 mCachedActiveList = getPriorityListLocked(true, 0, userId); 171 } 172 return mCachedActiveList; 173 } 174 175 /** 176 * Get the current priority sorted list of active sessions that use 177 * transport controls. The most important session is at index 0 and the 178 * least important at size -1. 179 * 180 * @param userId The user to check. 181 * @return All the active sessions that handle transport controls in 182 * priority order. 183 */ getTransportControlSessions(int userId)184 public ArrayList<MediaSessionRecord> getTransportControlSessions(int userId) { 185 if (mCachedTransportControlList == null) { 186 mCachedTransportControlList = getPriorityListLocked(true, 187 MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS, userId); 188 } 189 return mCachedTransportControlList; 190 } 191 192 /** 193 * Get the highest priority active session. 194 * 195 * @param userId The user to check. 196 * @return The current highest priority session or null. 197 */ getDefaultSession(int userId)198 public MediaSessionRecord getDefaultSession(int userId) { 199 if (mCachedDefault != null) { 200 return mCachedDefault; 201 } 202 ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId); 203 if (records.size() > 0) { 204 return records.get(0); 205 } 206 return null; 207 } 208 209 /** 210 * Get the highest priority session that can handle media buttons. 211 * 212 * @param userIdList The user lists to check. 213 * @param includeNotPlaying Return a non-playing session if nothing else is 214 * available 215 * @return The default media button session or null. 216 */ getDefaultMediaButtonSession( List<Integer> userIdList, boolean includeNotPlaying)217 public MediaSessionRecord getDefaultMediaButtonSession( 218 List<Integer> userIdList, 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, userIdList); 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 getDefaultVolumeSession(List<Integer> userIdList)251 public MediaSessionRecord getDefaultVolumeSession(List<Integer> userIdList) { 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, userIdList); 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 getDefaultRemoteSession(int userId)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 isGlobalPriorityActive()283 public boolean isGlobalPriorityActive() { 284 return mGlobalPrioritySession == null ? false : mGlobalPrioritySession.isActive(); 285 } 286 dump(PrintWriter pw, String prefix)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 getPriorityListLocked(boolean activeOnly, int withFlags, int userId)301 private ArrayList<MediaSessionRecord> getPriorityListLocked(boolean activeOnly, int withFlags, 302 int userId) { 303 List<Integer> userIdList = new ArrayList<>(); 304 userIdList.add(userId); 305 return getPriorityListLocked(activeOnly, withFlags, userIdList); 306 } 307 308 /** 309 * Get a priority sorted list of sessions. Can filter to only return active 310 * sessions or sessions with specific flags. 311 * 312 * @param activeOnly True to only return active sessions, false to return 313 * all sessions. 314 * @param withFlags Only return sessions with all the specified flags set. 0 315 * returns all sessions. 316 * @param userIdList The user to get sessions for. {@link UserHandle#USER_ALL} 317 * will return sessions for all users. 318 * @return The priority sorted list of sessions. 319 */ getPriorityListLocked(boolean activeOnly, int withFlags, List<Integer> userIdList)320 private ArrayList<MediaSessionRecord> getPriorityListLocked(boolean activeOnly, int withFlags, 321 List<Integer> userIdList) { 322 ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>(); 323 int lastLocalIndex = 0; 324 int lastActiveIndex = 0; 325 int lastPublishedIndex = 0; 326 327 boolean filterUser = !userIdList.contains(UserHandle.USER_ALL); 328 int size = mSessions.size(); 329 for (int i = 0; i < size; i++) { 330 final MediaSessionRecord session = mSessions.get(i); 331 332 if (filterUser && !userIdList.contains(session.getUserId())) { 333 // Filter out sessions for the wrong user 334 continue; 335 } 336 if ((session.getFlags() & withFlags) != withFlags) { 337 // Filter out sessions with the wrong flags 338 continue; 339 } 340 if (!session.isActive()) { 341 if (!activeOnly) { 342 // If we're getting unpublished as well always put them at 343 // the end 344 result.add(session); 345 } 346 continue; 347 } 348 349 if (session.isSystemPriority()) { 350 // System priority sessions are special and always go at the 351 // front. We expect there to only be one of these at a time. 352 result.add(0, session); 353 lastLocalIndex++; 354 lastActiveIndex++; 355 lastPublishedIndex++; 356 } else if (session.isPlaybackActive(true)) { 357 // TODO this with real local route check 358 if (true) { 359 // Active local sessions get top priority 360 result.add(lastLocalIndex, session); 361 lastLocalIndex++; 362 lastActiveIndex++; 363 lastPublishedIndex++; 364 } else { 365 // Then active remote sessions 366 result.add(lastActiveIndex, session); 367 lastActiveIndex++; 368 lastPublishedIndex++; 369 } 370 } else { 371 // inactive sessions go at the end in order of whoever last did 372 // something. 373 result.add(lastPublishedIndex, session); 374 lastPublishedIndex++; 375 } 376 } 377 378 return result; 379 } 380 shouldUpdatePriority(int oldState, int newState)381 private boolean shouldUpdatePriority(int oldState, int newState) { 382 if (containsState(newState, ALWAYS_PRIORITY_STATES)) { 383 return true; 384 } 385 if (!containsState(oldState, TRANSITION_PRIORITY_STATES) 386 && containsState(newState, TRANSITION_PRIORITY_STATES)) { 387 return true; 388 } 389 return false; 390 } 391 containsState(int state, int[] states)392 private boolean containsState(int state, int[] states) { 393 for (int i = 0; i < states.length; i++) { 394 if (states[i] == state) { 395 return true; 396 } 397 } 398 return false; 399 } 400 clearCache()401 private void clearCache() { 402 mCachedDefault = null; 403 mCachedVolumeDefault = null; 404 mCachedButtonReceiver = null; 405 mCachedActiveList = null; 406 mCachedTransportControlList = null; 407 } 408 } 409