blob: 719ec362e6e8cb3a8016e8d288fd28193eb7dfa1 [file] [log] [blame]
RoboErika8f95142014-05-05 14:23:49 -07001/*
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
RoboErikd2b8c942014-08-19 11:23:40 -070019import android.media.session.MediaController.PlaybackInfo;
RoboErik42ea7ee2014-05-16 16:27:35 -070020import android.media.session.MediaSession;
Jaewan Kim92dea332017-02-02 11:52:08 +090021import android.media.session.PlaybackState;
22import android.os.Debug;
RoboErika5b02322014-05-07 17:05:49 -070023import android.os.UserHandle;
Jaewan Kim92dea332017-02-02 11:52:08 +090024import android.util.IntArray;
25import android.util.Log;
Jaewan Kimaceef912017-03-30 03:05:06 +090026import android.util.SparseArray;
RoboErika8f95142014-05-05 14:23:49 -070027
28import java.io.PrintWriter;
29import java.util.ArrayList;
Insun Kang30be970a2015-11-26 15:35:44 +090030import java.util.List;
RoboErika8f95142014-05-05 14:23:49 -070031
32/**
33 * Keeps track of media sessions and their priority for notifications, media
Jeff Brown01a500e2014-07-10 22:50:50 -070034 * button dispatch, etc.
Jaewan Kima7dce192017-02-16 17:10:54 +090035 * <p>This class isn't thread-safe. The caller should take care of the synchronization.
RoboErika8f95142014-05-05 14:23:49 -070036 */
Jaewan Kim8f729082016-06-21 12:36:26 +090037class MediaSessionStack {
Jaewan Kim92dea332017-02-02 11:52:08 +090038 private static final boolean DEBUG = MediaSessionService.DEBUG;
39 private static final String TAG = "MediaSessionStack";
40
41 /**
Jaewan Kimaceef912017-03-30 03:05:06 +090042 * Listen the change in the media button session.
Jaewan Kim92dea332017-02-02 11:52:08 +090043 */
44 interface OnMediaButtonSessionChangedListener {
45 /**
46 * Called when the media button session is changed.
47 */
48 void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession,
49 MediaSessionRecord newMediaButtonSession);
50 }
51
RoboErika8f95142014-05-05 14:23:49 -070052 /**
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 = {
RoboErik79fa4632014-05-27 16:49:09 -070057 PlaybackState.STATE_FAST_FORWARDING,
58 PlaybackState.STATE_REWINDING,
59 PlaybackState.STATE_SKIPPING_TO_PREVIOUS,
60 PlaybackState.STATE_SKIPPING_TO_NEXT };
RoboErika8f95142014-05-05 14:23:49 -070061 /**
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 = {
RoboErik79fa4632014-05-27 16:49:09 -070066 PlaybackState.STATE_BUFFERING,
67 PlaybackState.STATE_CONNECTING,
68 PlaybackState.STATE_PLAYING };
RoboErika8f95142014-05-05 14:23:49 -070069
Jaewan Kim92dea332017-02-02 11:52:08 +090070 /**
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>();
RoboErika8f95142014-05-05 14:23:49 -070077
Sungsoo Lim875e6972017-11-03 02:22:35 +000078 private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
Jaewan Kim92dea332017-02-02 11:52:08 +090079 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
RoboErikb69ffd42014-05-30 14:57:59 -070087 private MediaSessionRecord mCachedVolumeDefault;
Jaewan Kimaceef912017-03-30 03:05:06 +090088
89 /**
90 * Cache the result of the {@link #getActiveSessions} per user.
91 */
92 private final SparseArray<ArrayList<MediaSessionRecord>> mCachedActiveLists =
93 new SparseArray<>();
RoboErika8f95142014-05-05 14:23:49 -070094
Sungsoo Lim875e6972017-11-03 02:22:35 +000095 MediaSessionStack(AudioPlayerStateMonitor monitor, OnMediaButtonSessionChangedListener listener) {
96 mAudioPlayerStateMonitor = monitor;
Jaewan Kim92dea332017-02-02 11:52:08 +090097 mOnMediaButtonSessionChangedListener = listener;
Insun Kang30be970a2015-11-26 15:35:44 +090098 }
99
100 /**
RoboErika8f95142014-05-05 14:23:49 -0700101 * Add a record to the priority tracker.
102 *
103 * @param record The record to add.
104 */
Jaewan Kim92dea332017-02-02 11:52:08 +0900105 public void addSession(MediaSessionRecord record) {
RoboErika8f95142014-05-05 14:23:49 -0700106 mSessions.add(record);
Jaewan Kimaceef912017-03-30 03:05:06 +0900107 clearCache(record.getUserId());
Jaewan Kim92dea332017-02-02 11:52:08 +0900108
109 // Update the media button session.
110 // The added session could be the session from the package with the audio playback.
111 // This can happen if an app starts audio playback before creating media session.
112 updateMediaButtonSessionIfNeeded();
RoboErika8f95142014-05-05 14:23:49 -0700113 }
114
115 /**
116 * Remove a record from the priority tracker.
117 *
118 * @param record The record to remove.
119 */
120 public void removeSession(MediaSessionRecord record) {
121 mSessions.remove(record);
Jaewan Kim92dea332017-02-02 11:52:08 +0900122 if (mMediaButtonSession == record) {
Jaewan Kimfc1f7ab2017-03-30 03:22:03 +0900123 // When the media button session is removed, nullify the media button session and do not
124 // search for the alternative media session within the app. It's because the alternative
125 // media session might be a dummy which isn't able to handle the media key events.
Jaewan Kim98e4aaf2017-05-12 17:06:47 +0900126 updateMediaButtonSession(null);
Jaewan Kim92dea332017-02-02 11:52:08 +0900127 }
Jaewan Kimaceef912017-03-30 03:05:06 +0900128 clearCache(record.getUserId());
RoboErika8f95142014-05-05 14:23:49 -0700129 }
130
131 /**
Jaewan Kima7dce192017-02-16 17:10:54 +0900132 * Return if the record exists in the priority tracker.
133 */
134 public boolean contains(MediaSessionRecord record) {
135 return mSessions.contains(record);
136 }
137
138 /**
Jaewan Kim92dea332017-02-02 11:52:08 +0900139 * Notify the priority tracker that a session's playback state changed.
RoboErika8f95142014-05-05 14:23:49 -0700140 *
141 * @param record The record that changed.
142 * @param oldState Its old playback state.
143 * @param newState Its new playback state.
144 */
Jaewan Kim92dea332017-02-02 11:52:08 +0900145 public void onPlaystateChanged(MediaSessionRecord record, int oldState, int newState) {
RoboErika8f95142014-05-05 14:23:49 -0700146 if (shouldUpdatePriority(oldState, newState)) {
147 mSessions.remove(record);
148 mSessions.add(0, record);
Jaewan Kimaceef912017-03-30 03:05:06 +0900149 clearCache(record.getUserId());
RoboErik23b11352014-09-24 09:24:54 -0700150 } else if (!MediaSession.isActiveState(newState)) {
151 // Just clear the volume cache when a state goes inactive
RoboErikb69ffd42014-05-30 14:57:59 -0700152 mCachedVolumeDefault = null;
RoboErika8f95142014-05-05 14:23:49 -0700153 }
Jaewan Kim92dea332017-02-02 11:52:08 +0900154
Sungsoo39f479f2017-06-09 12:59:47 +0900155 // In most cases, playback state isn't needed for finding media button session,
Jaewan Kim92dea332017-02-02 11:52:08 +0900156 // but we only use it as a hint if an app has multiple local media sessions.
157 // In that case, we pick the media session whose PlaybackState matches
158 // the audio playback configuration.
159 if (mMediaButtonSession != null && mMediaButtonSession.getUid() == record.getUid()) {
Jaewan Kimfc1f7ab2017-03-30 03:22:03 +0900160 MediaSessionRecord newMediaButtonSession =
161 findMediaButtonSession(mMediaButtonSession.getUid());
162 if (newMediaButtonSession != mMediaButtonSession) {
Jaewan Kim98e4aaf2017-05-12 17:06:47 +0900163 updateMediaButtonSession(newMediaButtonSession);
Jaewan Kimfc1f7ab2017-03-30 03:22:03 +0900164 }
Jaewan Kim92dea332017-02-02 11:52:08 +0900165 }
RoboErika8f95142014-05-05 14:23:49 -0700166 }
167
168 /**
Jaewan Kim92dea332017-02-02 11:52:08 +0900169 * Handle the change in activeness for a session.
RoboErika8f95142014-05-05 14:23:49 -0700170 *
171 * @param record The record that changed.
172 */
173 public void onSessionStateChange(MediaSessionRecord record) {
174 // For now just clear the cache. Eventually we'll selectively clear
175 // depending on what changed.
Jaewan Kimaceef912017-03-30 03:05:06 +0900176 clearCache(record.getUserId());
RoboErika8f95142014-05-05 14:23:49 -0700177 }
178
179 /**
Jaewan Kim92dea332017-02-02 11:52:08 +0900180 * Update the media button session if needed.
181 * <p>The media button session is the session that will receive the media button events.
182 * <p>We send the media button events to the lastly played app. If the app has the media
183 * session, the session will receive the media button events.
184 */
185 public void updateMediaButtonSessionIfNeeded() {
186 if (DEBUG) {
187 Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + Debug.getCallers(2));
188 }
Sungsoo Lim875e6972017-11-03 02:22:35 +0000189 IntArray audioPlaybackUids = mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
Jaewan Kim92dea332017-02-02 11:52:08 +0900190 for (int i = 0; i < audioPlaybackUids.size(); i++) {
191 MediaSessionRecord mediaButtonSession =
192 findMediaButtonSession(audioPlaybackUids.get(i));
193 if (mediaButtonSession != null) {
194 // Found the media button session.
Sungsoo Lim875e6972017-11-03 02:22:35 +0000195 mAudioPlayerStateMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
Jaewan Kim92dea332017-02-02 11:52:08 +0900196 if (mMediaButtonSession != mediaButtonSession) {
Jaewan Kim98e4aaf2017-05-12 17:06:47 +0900197 updateMediaButtonSession(mediaButtonSession);
Jaewan Kim92dea332017-02-02 11:52:08 +0900198 }
199 return;
200 }
201 }
202 }
203
204 /**
Jaewan Kim92dea332017-02-02 11:52:08 +0900205 * Find the media button session with the given {@param uid}.
Sungsoo39f479f2017-06-09 12:59:47 +0900206 * If the app has multiple media sessions, the media session whose playback state is not null
207 * and matches the audio playback state becomes the media button session. Otherwise the top
208 * priority session becomes the media button session.
Jaewan Kim92dea332017-02-02 11:52:08 +0900209 *
210 * @return The media button session. Returns {@code null} if the app doesn't have a media
211 * session.
212 */
213 private MediaSessionRecord findMediaButtonSession(int uid) {
214 MediaSessionRecord mediaButtonSession = null;
215 for (MediaSessionRecord session : mSessions) {
Jaewan Kim745e28c2017-05-10 20:14:35 +0900216 if (uid == session.getUid()) {
Sungsoo39f479f2017-06-09 12:59:47 +0900217 if (session.getPlaybackState() != null && session.isPlaybackActive() ==
Sungsoo Lim875e6972017-11-03 02:22:35 +0000218 mAudioPlayerStateMonitor.isPlaybackActive(session.getUid())) {
Jaewan Kim92dea332017-02-02 11:52:08 +0900219 // If there's a media session whose PlaybackState matches
220 // the audio playback state, return it immediately.
221 return session;
222 }
223 if (mediaButtonSession == null) {
224 // Among the media sessions whose PlaybackState doesn't match
225 // the audio playback state, pick the top priority.
226 mediaButtonSession = session;
227 }
228 }
229 }
230 return mediaButtonSession;
231 }
232
233 /**
RoboErika8f95142014-05-05 14:23:49 -0700234 * Get the current priority sorted list of active sessions. The most
235 * important session is at index 0 and the least important at size - 1.
236 *
Jaewan Kimaceef912017-03-30 03:05:06 +0900237 * @param userId The user to check. It can be {@link UserHandle#USER_ALL} to get all sessions
238 * for all users in this {@link MediaSessionStack}.
RoboErika8f95142014-05-05 14:23:49 -0700239 * @return All the active sessions in priority order.
240 */
RoboErika5b02322014-05-07 17:05:49 -0700241 public ArrayList<MediaSessionRecord> getActiveSessions(int userId) {
Jaewan Kimaceef912017-03-30 03:05:06 +0900242 ArrayList<MediaSessionRecord> cachedActiveList = mCachedActiveLists.get(userId);
243 if (cachedActiveList == null) {
244 cachedActiveList = getPriorityList(true, userId);
245 mCachedActiveLists.put(userId, cachedActiveList);
RoboErika8f95142014-05-05 14:23:49 -0700246 }
Jaewan Kimaceef912017-03-30 03:05:06 +0900247 return cachedActiveList;
RoboErika8f95142014-05-05 14:23:49 -0700248 }
249
250 /**
Jaewan Kim92dea332017-02-02 11:52:08 +0900251 * Get the media button session which receives the media button events.
RoboErika8f95142014-05-05 14:23:49 -0700252 *
Jaewan Kim92dea332017-02-02 11:52:08 +0900253 * @return The media button session or null.
RoboErika8f95142014-05-05 14:23:49 -0700254 */
Jaewan Kim92dea332017-02-02 11:52:08 +0900255 public MediaSessionRecord getMediaButtonSession() {
256 return mMediaButtonSession;
RoboErika8f95142014-05-05 14:23:49 -0700257 }
258
Jaewan Kim98e4aaf2017-05-12 17:06:47 +0900259 private void updateMediaButtonSession(MediaSessionRecord newMediaButtonSession) {
260 MediaSessionRecord oldMediaButtonSession = mMediaButtonSession;
261 mMediaButtonSession = newMediaButtonSession;
262 mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(
263 oldMediaButtonSession, newMediaButtonSession);
264 }
265
Jaewan Kima7dce192017-02-16 17:10:54 +0900266 public MediaSessionRecord getDefaultVolumeSession() {
RoboErikb69ffd42014-05-30 14:57:59 -0700267 if (mCachedVolumeDefault != null) {
268 return mCachedVolumeDefault;
269 }
Jaewan Kim92dea332017-02-02 11:52:08 +0900270 ArrayList<MediaSessionRecord> records = getPriorityList(true, UserHandle.USER_ALL);
RoboErikb69ffd42014-05-30 14:57:59 -0700271 int size = records.size();
272 for (int i = 0; i < size; i++) {
273 MediaSessionRecord record = records.get(i);
Jaewan Kim92dea332017-02-02 11:52:08 +0900274 if (record.isPlaybackActive()) {
RoboErikb69ffd42014-05-30 14:57:59 -0700275 mCachedVolumeDefault = record;
276 return record;
277 }
278 }
279 return null;
280 }
281
RoboErik19c95182014-06-23 15:38:48 -0700282 public MediaSessionRecord getDefaultRemoteSession(int userId) {
Jaewan Kim92dea332017-02-02 11:52:08 +0900283 ArrayList<MediaSessionRecord> records = getPriorityList(true, userId);
RoboErik19c95182014-06-23 15:38:48 -0700284
285 int size = records.size();
286 for (int i = 0; i < size; i++) {
287 MediaSessionRecord record = records.get(i);
RoboErikd2b8c942014-08-19 11:23:40 -0700288 if (record.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
RoboErik19c95182014-06-23 15:38:48 -0700289 return record;
290 }
291 }
292 return null;
293 }
294
RoboErika5b02322014-05-07 17:05:49 -0700295 public void dump(PrintWriter pw, String prefix) {
Jaewan Kim92dea332017-02-02 11:52:08 +0900296 ArrayList<MediaSessionRecord> sortedSessions = getPriorityList(false,
RoboErika5b02322014-05-07 17:05:49 -0700297 UserHandle.USER_ALL);
RoboErika8f95142014-05-05 14:23:49 -0700298 int count = sortedSessions.size();
Jaewan Kim92dea332017-02-02 11:52:08 +0900299 pw.println(prefix + "Media button session is " + mMediaButtonSession);
RoboErika8f95142014-05-05 14:23:49 -0700300 pw.println(prefix + "Sessions Stack - have " + count + " sessions:");
301 String indent = prefix + " ";
302 for (int i = 0; i < count; i++) {
303 MediaSessionRecord record = sortedSessions.get(i);
304 record.dump(pw, indent);
305 pw.println();
306 }
307 }
308
309 /**
310 * Get a priority sorted list of sessions. Can filter to only return active
Jaewan Kim92dea332017-02-02 11:52:08 +0900311 * sessions or sessions.
312 * <p>Here's the priority order.
Jaewan Kim92dea332017-02-02 11:52:08 +0900313 * <li>Active sessions whose PlaybackState is active</li>
314 * <li>Active sessions whose PlaybackState is inactive</li>
315 * <li>Inactive sessions</li>
RoboErika8f95142014-05-05 14:23:49 -0700316 *
317 * @param activeOnly True to only return active sessions, false to return
318 * all sessions.
Jaewan Kima7dce192017-02-16 17:10:54 +0900319 * @param userId The user to get sessions for. {@link UserHandle#USER_ALL}
RoboErika5b02322014-05-07 17:05:49 -0700320 * will return sessions for all users.
RoboErika8f95142014-05-05 14:23:49 -0700321 * @return The priority sorted list of sessions.
322 */
Jaewan Kim92dea332017-02-02 11:52:08 +0900323 public ArrayList<MediaSessionRecord> getPriorityList(boolean activeOnly, int userId) {
RoboErika8f95142014-05-05 14:23:49 -0700324 ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>();
Jaewan Kim92dea332017-02-02 11:52:08 +0900325 int lastPlaybackActiveIndex = 0;
RoboErika8f95142014-05-05 14:23:49 -0700326 int lastActiveIndex = 0;
RoboErika8f95142014-05-05 14:23:49 -0700327
328 int size = mSessions.size();
329 for (int i = 0; i < size; i++) {
330 final MediaSessionRecord session = mSessions.get(i);
331
Jaewan Kima7dce192017-02-16 17:10:54 +0900332 if (userId != UserHandle.USER_ALL && userId != session.getUserId()) {
RoboErika5b02322014-05-07 17:05:49 -0700333 // Filter out sessions for the wrong user
334 continue;
335 }
Jaewan Kim92dea332017-02-02 11:52:08 +0900336
RoboErika8f95142014-05-05 14:23:49 -0700337 if (!session.isActive()) {
338 if (!activeOnly) {
339 // If we're getting unpublished as well always put them at
340 // the end
341 result.add(session);
342 }
343 continue;
344 }
345
Jaewan Kim101b4d52017-05-18 13:23:11 +0900346 if (session.isPlaybackActive()) {
Jaewan Kim92dea332017-02-02 11:52:08 +0900347 result.add(lastPlaybackActiveIndex++, session);
348 lastActiveIndex++;
RoboErika8f95142014-05-05 14:23:49 -0700349 } else {
Jaewan Kim92dea332017-02-02 11:52:08 +0900350 result.add(lastActiveIndex++, session);
RoboErika8f95142014-05-05 14:23:49 -0700351 }
352 }
353
354 return result;
355 }
356
357 private boolean shouldUpdatePriority(int oldState, int newState) {
358 if (containsState(newState, ALWAYS_PRIORITY_STATES)) {
359 return true;
360 }
361 if (!containsState(oldState, TRANSITION_PRIORITY_STATES)
362 && containsState(newState, TRANSITION_PRIORITY_STATES)) {
363 return true;
364 }
365 return false;
366 }
367
368 private boolean containsState(int state, int[] states) {
369 for (int i = 0; i < states.length; i++) {
370 if (states[i] == state) {
371 return true;
372 }
373 }
374 return false;
375 }
376
Jaewan Kimaceef912017-03-30 03:05:06 +0900377 private void clearCache(int userId) {
RoboErikb69ffd42014-05-30 14:57:59 -0700378 mCachedVolumeDefault = null;
Jaewan Kimaceef912017-03-30 03:05:06 +0900379 mCachedActiveLists.remove(userId);
380 // mCachedActiveLists may also include the list of sessions for UserHandle.USER_ALL,
381 // so they also need to be cleared.
382 mCachedActiveLists.remove(UserHandle.USER_ALL);
RoboErika8f95142014-05-05 14:23:49 -0700383 }
384}