• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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