1 /*
2  * Copyright 2021 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.annotation.Nullable;
20 import android.media.Session2Token;
21 import android.os.Build;
22 import android.util.Log;
23 
24 import androidx.annotation.RequiresApi;
25 
26 import com.android.internal.annotations.GuardedBy;
27 import com.android.modules.annotation.MinSdk;
28 import com.android.server.media.MediaCommunicationService.Session2Record;
29 
30 import java.util.ArrayList;
31 import java.util.List;
32 
33 //TODO: Define the priority specifically.
34 /**
35  * Keeps track of media sessions and their priority for notifications, media
36  * button dispatch, etc.
37  * Higher priority session has more chance to be selected as media button session,
38  * which receives the media button events.
39  */
40 @MinSdk(Build.VERSION_CODES.S)
41 @RequiresApi(Build.VERSION_CODES.S)
42 class SessionPriorityList {
43     private static final String TAG = "SessionPriorityList";
44     private final Object mLock = new Object();
45 
46     @GuardedBy("mLock")
47     private final List<Session2Record> mSessions = new ArrayList<>();
48 
49     @Nullable
50     private Session2Record mMediaButtonSession;
51     @Nullable
52     private Session2Record mCachedVolumeSession;
53 
54     //TODO: integrate AudioPlayerStateMonitor
55 
addSession(Session2Record record)56     public void addSession(Session2Record record) {
57         synchronized (mLock) {
58             mSessions.add(record);
59         }
60     }
61 
removeSession(Session2Record record)62     public void removeSession(Session2Record record) {
63         synchronized (mLock) {
64             mSessions.remove(record);
65         }
66         if (record == mMediaButtonSession) {
67             updateMediaButtonSession(null);
68         }
69     }
70 
destroyAllSessions()71     public void destroyAllSessions() {
72         synchronized (mLock) {
73             for (Session2Record session : mSessions) {
74                 session.close();
75             }
76             mSessions.clear();
77         }
78     }
79 
destroySessionsByUserId(int userId)80     public boolean destroySessionsByUserId(int userId) {
81         boolean changed = false;
82         synchronized (mLock) {
83             for (int i = mSessions.size() - 1; i >= 0; i--) {
84                 Session2Record session = mSessions.get(i);
85                 if (session.getUserId() == userId) {
86                     mSessions.remove(i);
87                     session.close();
88                     changed = true;
89                 }
90             }
91         }
92         return changed;
93     }
94 
getAllTokens()95     public List<Session2Token> getAllTokens() {
96         List<Session2Token> sessions = new ArrayList<>();
97         synchronized (mLock) {
98             for (Session2Record session : mSessions) {
99                 sessions.add(session.getSessionToken());
100             }
101         }
102         return sessions;
103     }
104 
getTokensByUserId(int userId)105     public List<Session2Token> getTokensByUserId(int userId) {
106         List<Session2Token> sessions = new ArrayList<>();
107         synchronized (mLock) {
108             for (Session2Record session : mSessions) {
109                 if (session.getUserId() == userId) {
110                     sessions.add(session.getSessionToken());
111                 }
112             }
113         }
114         return sessions;
115     }
116 
117     /** Gets the media button session which receives the media button events. */
118     @Nullable
getMediaButtonSession()119     public Session2Record getMediaButtonSession() {
120         return mMediaButtonSession;
121     }
122 
123     /** Gets the media volume session which receives the volume key events. */
124     @Nullable
getMediaVolumeSession()125     public Session2Record getMediaVolumeSession() {
126         //TODO: if null, calculate it.
127         return mCachedVolumeSession;
128     }
129 
contains(Session2Record session)130     public boolean contains(Session2Record session) {
131         synchronized (mLock) {
132             return mSessions.contains(session);
133         }
134     }
135 
onPlaybackStateChanged(Session2Record session, boolean promotePriority)136     public void onPlaybackStateChanged(Session2Record session, boolean promotePriority) {
137         if (promotePriority) {
138             synchronized (mLock) {
139                 if (mSessions.remove(session)) {
140                     mSessions.add(0, session);
141                 } else {
142                     Log.w(TAG, "onPlaybackStateChanged: Ignoring unknown session");
143                     return;
144                 }
145             }
146         } else if (session.checkPlaybackActiveState(false)) {
147             // Just clear the cached volume session when a state goes inactive
148             mCachedVolumeSession = null;
149         }
150 
151         // In most cases, playback state isn't needed for finding the media button session,
152         // but we only use it as a hint if an app has multiple local media sessions.
153         // In that case, we pick the media session whose PlaybackState matches
154         // the audio playback configuration.
155         if (mMediaButtonSession != null
156                 && mMediaButtonSession.getSessionToken().getUid()
157                 == session.getSessionToken().getUid()) {
158             Session2Record newMediaButtonSession =
159                     findMediaButtonSession(mMediaButtonSession.getSessionToken().getUid());
160             if (newMediaButtonSession != mMediaButtonSession) {
161                 // Check if the policy states that this session should not be updated as a media
162                 // button session.
163                 updateMediaButtonSession(newMediaButtonSession);
164             }
165         }
166     }
167 
updateMediaButtonSession(@ullable Session2Record newSession)168     private void updateMediaButtonSession(@Nullable Session2Record newSession) {
169         mMediaButtonSession = newSession;
170         //TODO: invoke callbacks for media button session changed listeners
171     }
172 
173     /**
174      * Finds the media button session with the given {@param uid}.
175      * If the app has multiple media sessions, the media session whose playback state is not null
176      * and matches the audio playback state becomes the media button session. Otherwise the top
177      * priority session becomes the media button session.
178      *
179      * @return The media button session. Returns {@code null} if the app doesn't have a media
180      *   session.
181      */
182     @Nullable
findMediaButtonSession(int uid)183     private Session2Record findMediaButtonSession(int uid) {
184         Session2Record mediaButtonSession = null;
185         synchronized (mLock) {
186             for (Session2Record session : mSessions) {
187                 if (uid != session.getSessionToken().getUid()) {
188                     continue;
189                 }
190                 // TODO: check audio player state monitor
191                 if (mediaButtonSession == null) {
192                     // Pick the top priority session as a default.
193                     mediaButtonSession = session;
194                 }
195             }
196         }
197         return mediaButtonSession;
198     }
199 }
200