/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.media; import android.media.session.MediaController.PlaybackInfo; import android.media.session.PlaybackState; import android.media.session.MediaSession; import android.os.UserHandle; import java.io.PrintWriter; import java.util.ArrayList; /** * Keeps track of media sessions and their priority for notifications, media * button dispatch, etc. */ public class MediaSessionStack { /** * These are states that usually indicate the user took an action and should * bump priority regardless of the old state. */ private static final int[] ALWAYS_PRIORITY_STATES = { PlaybackState.STATE_FAST_FORWARDING, PlaybackState.STATE_REWINDING, PlaybackState.STATE_SKIPPING_TO_PREVIOUS, PlaybackState.STATE_SKIPPING_TO_NEXT }; /** * These are states that usually indicate the user took an action if they * were entered from a non-priority state. */ private static final int[] TRANSITION_PRIORITY_STATES = { PlaybackState.STATE_BUFFERING, PlaybackState.STATE_CONNECTING, PlaybackState.STATE_PLAYING }; private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>(); private MediaSessionRecord mGlobalPrioritySession; // The last record that either entered one of the playing states or was // added. private MediaSessionRecord mLastInterestingRecord; private MediaSessionRecord mCachedButtonReceiver; private MediaSessionRecord mCachedDefault; private MediaSessionRecord mCachedVolumeDefault; private ArrayList<MediaSessionRecord> mCachedActiveList; private ArrayList<MediaSessionRecord> mCachedTransportControlList; /** * Add a record to the priority tracker. * * @param record The record to add. */ public void addSession(MediaSessionRecord record) { mSessions.add(record); clearCache(); mLastInterestingRecord = record; } /** * Remove a record from the priority tracker. * * @param record The record to remove. */ public void removeSession(MediaSessionRecord record) { mSessions.remove(record); if (record == mGlobalPrioritySession) { mGlobalPrioritySession = null; } clearCache(); } /** * Notify the priority tracker that a session's state changed. * * @param record The record that changed. * @param oldState Its old playback state. * @param newState Its new playback state. * @return true if the priority order was updated, false otherwise. */ public boolean onPlaystateChange(MediaSessionRecord record, int oldState, int newState) { if (shouldUpdatePriority(oldState, newState)) { mSessions.remove(record); mSessions.add(0, record); clearCache(); // This becomes the last interesting record since it entered a // playing state mLastInterestingRecord = record; return true; } else if (!MediaSession.isActiveState(newState)) { // Just clear the volume cache when a state goes inactive mCachedVolumeDefault = null; } return false; } /** * Handle any stack changes that need to occur in response to a session * state change. TODO add the old and new session state as params * * @param record The record that changed. */ public void onSessionStateChange(MediaSessionRecord record) { if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) { mGlobalPrioritySession = record; } // For now just clear the cache. Eventually we'll selectively clear // depending on what changed. clearCache(); } /** * Get the current priority sorted list of active sessions. The most * important session is at index 0 and the least important at size - 1. * * @param userId The user to check. * @return All the active sessions in priority order. */ public ArrayList<MediaSessionRecord> getActiveSessions(int userId) { if (mCachedActiveList == null) { mCachedActiveList = getPriorityListLocked(true, 0, userId); } return mCachedActiveList; } /** * Get the current priority sorted list of active sessions that use * transport controls. The most important session is at index 0 and the * least important at size -1. * * @param userId The user to check. * @return All the active sessions that handle transport controls in * priority order. */ public ArrayList<MediaSessionRecord> getTransportControlSessions(int userId) { if (mCachedTransportControlList == null) { mCachedTransportControlList = getPriorityListLocked(true, MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS, userId); } return mCachedTransportControlList; } /** * Get the highest priority active session. * * @param userId The user to check. * @return The current highest priority session or null. */ public MediaSessionRecord getDefaultSession(int userId) { if (mCachedDefault != null) { return mCachedDefault; } ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId); if (records.size() > 0) { return records.get(0); } return null; } /** * Get the highest priority session that can handle media buttons. * * @param userId The user to check. * @param includeNotPlaying Return a non-playing session if nothing else is * available * @return The default media button session or null. */ public MediaSessionRecord getDefaultMediaButtonSession(int userId, boolean includeNotPlaying) { if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) { return mGlobalPrioritySession; } if (mCachedButtonReceiver != null) { return mCachedButtonReceiver; } ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, MediaSession.FLAG_HANDLES_MEDIA_BUTTONS, userId); if (records.size() > 0) { MediaSessionRecord record = records.get(0); if (record.isPlaybackActive(false)) { // Since we're going to send a button event to this record make // it the last interesting one. mLastInterestingRecord = record; mCachedButtonReceiver = record; } else if (mLastInterestingRecord != null) { if (records.contains(mLastInterestingRecord)) { mCachedButtonReceiver = mLastInterestingRecord; } else { // That record is no longer used. Clear its reference. mLastInterestingRecord = null; } } if (includeNotPlaying && mCachedButtonReceiver == null) { // If we really want a record and we didn't find one yet use the // highest priority session even if it's not playing. mCachedButtonReceiver = record; } } return mCachedButtonReceiver; } public MediaSessionRecord getDefaultVolumeSession(int userId) { if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) { return mGlobalPrioritySession; } if (mCachedVolumeDefault != null) { return mCachedVolumeDefault; } ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId); int size = records.size(); for (int i = 0; i < size; i++) { MediaSessionRecord record = records.get(i); if (record.isPlaybackActive(false)) { mCachedVolumeDefault = record; return record; } } return null; } public MediaSessionRecord getDefaultRemoteSession(int userId) { ArrayList<MediaSessionRecord> records = getPriorityListLocked(true, 0, userId); int size = records.size(); for (int i = 0; i < size; i++) { MediaSessionRecord record = records.get(i); if (record.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE) { return record; } } return null; } public boolean isGlobalPriorityActive() { return mGlobalPrioritySession == null ? false : mGlobalPrioritySession.isActive(); } public void dump(PrintWriter pw, String prefix) { ArrayList<MediaSessionRecord> sortedSessions = getPriorityListLocked(false, 0, UserHandle.USER_ALL); int count = sortedSessions.size(); pw.println(prefix + "Global priority session is " + mGlobalPrioritySession); pw.println(prefix + "Sessions Stack - have " + count + " sessions:"); String indent = prefix + " "; for (int i = 0; i < count; i++) { MediaSessionRecord record = sortedSessions.get(i); record.dump(pw, indent); pw.println(); } } /** * Get a priority sorted list of sessions. Can filter to only return active * sessions or sessions with specific flags. * * @param activeOnly True to only return active sessions, false to return * all sessions. * @param withFlags Only return sessions with all the specified flags set. 0 * returns all sessions. * @param userId The user to get sessions for. {@link UserHandle#USER_ALL} * will return sessions for all users. * @return The priority sorted list of sessions. */ private ArrayList<MediaSessionRecord> getPriorityListLocked(boolean activeOnly, int withFlags, int userId) { ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>(); int lastLocalIndex = 0; int lastActiveIndex = 0; int lastPublishedIndex = 0; int size = mSessions.size(); for (int i = 0; i < size; i++) { final MediaSessionRecord session = mSessions.get(i); if (userId != UserHandle.USER_ALL && userId != session.getUserId()) { // Filter out sessions for the wrong user continue; } if ((session.getFlags() & withFlags) != withFlags) { // Filter out sessions with the wrong flags continue; } if (!session.isActive()) { if (!activeOnly) { // If we're getting unpublished as well always put them at // the end result.add(session); } continue; } if (session.isSystemPriority()) { // System priority sessions are special and always go at the // front. We expect there to only be one of these at a time. result.add(0, session); lastLocalIndex++; lastActiveIndex++; lastPublishedIndex++; } else if (session.isPlaybackActive(true)) { // TODO this with real local route check if (true) { // Active local sessions get top priority result.add(lastLocalIndex, session); lastLocalIndex++; lastActiveIndex++; lastPublishedIndex++; } else { // Then active remote sessions result.add(lastActiveIndex, session); lastActiveIndex++; lastPublishedIndex++; } } else { // inactive sessions go at the end in order of whoever last did // something. result.add(lastPublishedIndex, session); lastPublishedIndex++; } } return result; } private boolean shouldUpdatePriority(int oldState, int newState) { if (containsState(newState, ALWAYS_PRIORITY_STATES)) { return true; } if (!containsState(oldState, TRANSITION_PRIORITY_STATES) && containsState(newState, TRANSITION_PRIORITY_STATES)) { return true; } return false; } private boolean containsState(int state, int[] states) { for (int i = 0; i < states.length; i++) { if (states[i] == state) { return true; } } return false; } private void clearCache() { mCachedDefault = null; mCachedVolumeDefault = null; mCachedButtonReceiver = null; mCachedActiveList = null; mCachedTransportControlList = null; } }